├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── __tests__ ├── fs.test.ts ├── jsonpack.test.ts ├── mark.test.ts ├── meta.test.ts ├── page.test.ts └── sketchfile.test.ts ├── demo ├── artboard │ ├── artboard2symbol.ts │ ├── new-artboard.ts │ └── symbol2artboard.ts ├── files │ ├── 1Symbol1Artboard.sketch │ ├── CheckboxAndRadio.sketch │ ├── LayerSymbolArtboard.sketch │ ├── SimpleButton.sketch │ ├── SomeArtboards │ │ ├── document.json │ │ ├── meta.json │ │ ├── pages │ │ │ └── A76B144E-7469-4ECF-B8D4-6293FA873AE7.json │ │ ├── previews │ │ │ └── preview.png │ │ └── user.json │ ├── TechUIdemo.sketch │ ├── bitmap.sketch │ └── miniSamplePack │ │ ├── document.json │ │ ├── meta.json │ │ ├── pages │ │ └── 990FFC96-8159-4E45-9A1F-6E5A25D57C8F.json │ │ ├── previews │ │ └── preview.png │ │ └── user.json ├── label │ ├── label.ts │ └── techui │ │ ├── meta.ts │ │ └── techui.ts ├── new │ ├── layout.ts │ ├── new-from-json.ts │ ├── new.ts │ └── with-image.ts ├── split │ └── split-from.ts ├── symbol │ ├── symbol.ts │ └── symboljson.ts ├── unzip │ ├── minisample.sketch │ └── unzip.ts └── zip │ ├── rezip-with-bitmap.ts │ └── zip.ts ├── jest.config.js ├── package.json ├── scripts └── sync-origin-types.ts ├── src ├── constants │ ├── index.ts │ └── init.ts ├── index.ts ├── structures │ ├── Document.ts │ ├── JSONPack.ts │ ├── Layer.ts │ ├── Meta.ts │ ├── Page.ts │ ├── SketchFile.ts │ ├── User.ts │ └── models │ │ ├── PagesAndArtboards.ts │ │ ├── Rect.ts │ │ ├── Style.ts │ │ └── index.ts ├── types │ ├── document.ts │ ├── index.ts │ ├── meta.ts │ ├── origin.ts │ ├── page.ts │ ├── user.ts │ └── utils.ts └── utils │ ├── fs-custom.ts │ ├── image.ts │ ├── index.ts │ ├── object.ts │ └── types.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | package-lock.json 45 | 46 | # TypeScript v1 declaration files 47 | typings/ 48 | 49 | # TypeScript cache 50 | *.tsbuildinfo 51 | 52 | # Optional npm cache directory 53 | .npm 54 | 55 | # Optional eslint cache 56 | .eslintcache 57 | 58 | # Microbundle cache 59 | .rpt2_cache/ 60 | .rts2_cache_cjs/ 61 | .rts2_cache_es/ 62 | .rts2_cache_umd/ 63 | 64 | # Optional REPL history 65 | .node_repl_history 66 | 67 | # Output of 'npm pack' 68 | *.tgz 69 | 70 | # Yarn Integrity file 71 | .yarn-integrity 72 | 73 | # dotenv environment variables file 74 | .env 75 | .env.test 76 | 77 | # parcel-bundler cache (https://parceljs.org/) 78 | .cache 79 | 80 | # Next.js build output 81 | .next 82 | 83 | # Nuxt.js build / generate output 84 | .nuxt 85 | dist 86 | 87 | # Gatsby files 88 | .cache/ 89 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 90 | # https://nextjs.org/blog/next-9-1#public-directory-support 91 | # public 92 | 93 | # vuepress build output 94 | .vuepress/dist 95 | 96 | # Serverless directories 97 | .serverless/ 98 | 99 | # FuseBox cache 100 | .fusebox/ 101 | 102 | # DynamoDB Local files 103 | .dynamodb/ 104 | 105 | # TernJS port file 106 | .tern-port 107 | 108 | # Temp files 109 | temp 110 | .temp 111 | 112 | .DS_Store -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | demo 2 | dist/demo 3 | /src 4 | temp 5 | jest.config.jest 6 | tsconfig.json -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 neoddish 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-json-api 2 | 3 | A Node.js library for editing `.sketch` files on servers. 4 | 5 | ## Official Docs 6 | 7 | You may want to check the official docs from Sketch. 8 | 9 | - [Sketch Command-line Interface](https://developer.sketch.com/cli/) 10 | - [Sketch File Format](https://developer.sketch.com/file-format/) 11 | 12 | ## Install 13 | 14 | ```bash 15 | npm install sketch-json-api 16 | ``` 17 | 18 | ## Dev 19 | 20 | To install dependencies: 21 | 22 | ```bash 23 | npm install 24 | ``` 25 | 26 | To build the project: 27 | 28 | ```bash 29 | npm run build 30 | ``` 31 | 32 | To run specific demo: 33 | 34 | ```bash 35 | node dist/demo/path/to/file.js 36 | ``` 37 | 38 | To check results: 39 | 40 | Check the files in `temp/`. 41 | -------------------------------------------------------------------------------- /__tests__/fs.test.ts: -------------------------------------------------------------------------------- 1 | import * as fse from "fs-extra"; 2 | import * as path from "path"; 3 | 4 | import * as fsc from "../src/utils/fs-custom"; 5 | 6 | test("fsc", async () => { 7 | const samplePath = "temp/tests/some-dir-path"; 8 | await fsc.resetPath(samplePath); 9 | const filePath1 = path.join(samplePath, "test1.txt"); 10 | const filePath2 = path.join(samplePath, "test2.txt"); 11 | 12 | await fse.writeFile(filePath1, "any content 1"); 13 | await fse.writeFile(filePath2, "any content 2"); 14 | 15 | let isFile1There = await fse.pathExists(filePath1); 16 | expect(isFile1There).toBe(true); 17 | 18 | await fsc.resetPath(filePath1); 19 | isFile1There = await fse.pathExists(filePath1); 20 | expect(isFile1There).toBe(false); 21 | 22 | let isFile2There = await fse.pathExists(filePath2); 23 | expect(isFile2There).toBe(true); 24 | 25 | await fsc.resetPath(samplePath); 26 | isFile2There = await fse.pathExists(filePath2); 27 | expect(isFile2There).toBe(false); 28 | }); 29 | -------------------------------------------------------------------------------- /__tests__/jsonpack.test.ts: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | import * as fse from "fs-extra"; 3 | import { JSONPack } from "../src"; 4 | 5 | const testResultsPath = "temp/tests/jsonpack"; 6 | 7 | const samplePackPath = "demo/files/SomeArtboards"; 8 | 9 | describe("JSONPack class", () => { 10 | let testPack; 11 | beforeAll(async () => { 12 | testPack = await JSONPack.fromPath(samplePackPath); 13 | }); 14 | 15 | describe("getAllArtboards method works", () => { 16 | it("basic", async () => { 17 | const artboards = testPack.getAllArtboards(); 18 | 19 | expect( 20 | artboards.map((artboard) => { 21 | return { name: artboard.name, class: artboard._class }; 22 | }) 23 | ).toStrictEqual([ 24 | { name: "SomeSymbolMaster", class: "symbolMaster" }, 25 | { name: "SomeArtboard", class: "artboard" }, 26 | ]); 27 | }); 28 | }); 29 | 30 | describe("zip method works", () => { 31 | const zipTestResultsPath = path.join(testResultsPath, "zip"); 32 | 33 | it("by cli", async () => { 34 | const cliDestPath = path.join(zipTestResultsPath, "by_cli.sketch"); 35 | await testPack.zip(cliDestPath, { cli: true }); 36 | expect(fse.pathExistsSync(cliDestPath)).toBe(true); 37 | }); 38 | 39 | it("by node", async () => { 40 | const nodeDestPath = path.join(zipTestResultsPath, "by_node.sketch"); 41 | await testPack.zip(nodeDestPath); 42 | expect(fse.pathExistsSync(nodeDestPath)).toBe(true); 43 | }); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /__tests__/mark.test.ts: -------------------------------------------------------------------------------- 1 | import { markLayer } from "../src"; 2 | 3 | describe("Marking", () => { 4 | describe("Mark layers without userInfo", () => { 5 | it("symbolMaster", async () => { 6 | const someSymbolMasterJSON: any = { 7 | _class: "symbolMaster", 8 | }; 9 | 10 | markLayer(someSymbolMasterJSON, "myMark", { 11 | info: { key: "value" }, 12 | }); 13 | 14 | expect(someSymbolMasterJSON.userInfo.myMark).toStrictEqual({ 15 | info: { key: "value" }, 16 | }); 17 | }); 18 | 19 | it("artboard", async () => { 20 | const someArtboardJSON: any = { 21 | _class: "artboard", 22 | }; 23 | 24 | markLayer(someArtboardJSON, "myMark", { 25 | info: { key: "value" }, 26 | }); 27 | 28 | expect(someArtboardJSON.userInfo.myMark).toStrictEqual({ 29 | info: { key: "value" }, 30 | }); 31 | }); 32 | }); 33 | 34 | describe("Mark layers with userInfo", () => { 35 | it("not overwrite", async () => { 36 | const someSymbolMasterJSON: any = { 37 | _class: "symbolMaster", 38 | userInfo: { 39 | a: "aaa", 40 | }, 41 | }; 42 | 43 | markLayer(someSymbolMasterJSON, "myMark", { 44 | info: { key: "value" }, 45 | }); 46 | 47 | expect(someSymbolMasterJSON.userInfo.a).toEqual("aaa"); 48 | expect(someSymbolMasterJSON.userInfo.myMark).toStrictEqual({ 49 | info: { key: "value" }, 50 | }); 51 | }); 52 | 53 | it("overwrite userInfo key", async () => { 54 | const spy = jest.spyOn(global.console, "warn"); 55 | 56 | const someSymbolMasterJSON: any = { 57 | _class: "symbolMaster", 58 | userInfo: { 59 | a: "aaa", 60 | }, 61 | }; 62 | 63 | markLayer(someSymbolMasterJSON, "a", { 64 | info: { key: "value" }, 65 | }); 66 | 67 | expect(spy).toHaveBeenCalled(); 68 | expect(someSymbolMasterJSON.userInfo.a).toStrictEqual({ 69 | info: { key: "value" }, 70 | }); 71 | 72 | spy.mockRestore(); 73 | }); 74 | }); 75 | }); 76 | -------------------------------------------------------------------------------- /__tests__/meta.test.ts: -------------------------------------------------------------------------------- 1 | import { Meta } from "../src"; 2 | 3 | test("meta constructor", () => { 4 | const meta = new Meta(); 5 | // console.log(meta.toSketchJSON()); 6 | }); 7 | 8 | test("meta from data", () => { 9 | const data: any = { 10 | commit: "d06f2f7bf433bc948c6f867ddfb87013f4871eb3", 11 | pagesAndArtboards: { 12 | "A869BA2A-E632-4C2D-924E-7883848BB266": { name: "Page 1", artboards: {} }, 13 | }, 14 | version: 130, 15 | fonts: [], 16 | compatibilityVersion: 99, 17 | app: "com.bohemiancoding.sketch3", 18 | autosaved: 0, 19 | variant: "NONAPPSTORE", 20 | created: { 21 | commit: "d06f2f7bf433bc948c6f867ddfb87013f4871eb3", 22 | appVersion: "66.1", 23 | build: 97080, 24 | app: "com.bohemiancoding.sketch3", 25 | compatibilityVersion: 99, 26 | version: 130, 27 | variant: "NONAPPSTORE", 28 | }, 29 | saveHistory: ["NONAPPSTORE.97080"], 30 | appVersion: "66.1", 31 | build: 97080, 32 | }; 33 | const meta = Meta.fromData(data); 34 | // console.log(meta.toSketchJSON()); 35 | }); 36 | 37 | test("meta from path", () => { 38 | Meta.fromPath("demo/files/miniSamplePack/meta.json"); 39 | }); 40 | -------------------------------------------------------------------------------- /__tests__/page.test.ts: -------------------------------------------------------------------------------- 1 | import { Page, Layer } from "../src"; 2 | 3 | test("page", () => { 4 | const layer = new Layer(); 5 | 6 | const layers = [layer.toSketchJSON()]; 7 | 8 | const page = new Page({ layers } as any); 9 | // console.log(page.toSketchJSON()); 10 | }); 11 | -------------------------------------------------------------------------------- /__tests__/sketchfile.test.ts: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | import { SketchFile, JSONPack } from "../src"; 3 | 4 | const testResultsPath = "temp/tests/sketchfile"; 5 | 6 | const testSketchFilePath = "demo/files/SimpleButton.sketch"; 7 | const testSketchFile = new SketchFile(testSketchFilePath); 8 | 9 | describe("SketchFile class", () => { 10 | describe("unzip method works", () => { 11 | const unzipTestResultsPath = path.join(testResultsPath, "unzip"); 12 | 13 | it("by cli", async () => { 14 | const cliDestPath = path.join(unzipTestResultsPath, "by_cli"); 15 | await testSketchFile.unzip(cliDestPath, { cli: true }); 16 | expect(JSONPack.isValidStructure(cliDestPath)).toBe(true); 17 | }); 18 | 19 | it("by node", async () => { 20 | const nodeDestPath = path.join(unzipTestResultsPath, "by_node"); 21 | await testSketchFile.unzip(nodeDestPath); 22 | expect(JSONPack.isValidStructure(nodeDestPath)).toBe(true); 23 | }); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /demo/artboard/artboard2symbol.ts: -------------------------------------------------------------------------------- 1 | import { JSONPack, Layer, Page, SketchFile, SketchType } from "../../src"; 2 | 3 | const DEMO_SOURCE_FILE = "demo/files/LayerSymbolArtboard.sketch"; 4 | const DEMO_PATH = "temp/artboard/artboard2symbol"; 5 | 6 | (async () => { 7 | const originSketch = new SketchFile(DEMO_SOURCE_FILE); 8 | const originFilePath = `${DEMO_PATH}/origin`; 9 | await originSketch.unzip(originFilePath); 10 | 11 | if (!JSONPack.isValidStructure(originFilePath)) return; 12 | 13 | const originPack = await JSONPack.fromPath(originFilePath); 14 | 15 | const allArtboardLikes = originPack.getAllArtboards(); 16 | 17 | const newLayers = allArtboardLikes.map((layer) => { 18 | if (layer._class === "artboard") { 19 | const outerSymbol = new Layer({ class: "SymbolMaster", data: null }); 20 | outerSymbol.updateProps({ 21 | data: { 22 | name: layer.name, 23 | frame: layer.frame, 24 | layers: layer.layers, 25 | } as SketchType.SymbolMaster, 26 | }); 27 | return outerSymbol.toSketchJSON(); 28 | } 29 | 30 | return layer; 31 | }); 32 | 33 | const newPage = new Page({ layers: newLayers } as any); 34 | const newPack = new JSONPack({ pages: [newPage] } as any); 35 | 36 | await newPack.write(`${DEMO_PATH}/files`); 37 | await newPack.zip(`${DEMO_PATH}/result.sketch`); 38 | })(); 39 | -------------------------------------------------------------------------------- /demo/artboard/new-artboard.ts: -------------------------------------------------------------------------------- 1 | import { JSONPack, Layer, Page } from "../../src"; 2 | 3 | const DEMO_PATH = "temp/artboard/new-artboard"; 4 | 5 | const newArtboard = new Layer({ class: "Artboard", data: null }); 6 | 7 | newArtboard.updateProps({ 8 | data: { 9 | frame: { 10 | height: 249, 11 | width: 263, 12 | }, 13 | layers: [ 14 | { 15 | _class: "triangle", 16 | do_objectID: "FF47D4AF-09ED-4354-8A64-B36F43EBFB08", 17 | booleanOperation: -1, 18 | isFixedToViewport: false, 19 | isFlippedHorizontal: false, 20 | isFlippedVertical: false, 21 | isLocked: false, 22 | isVisible: true, 23 | layerListExpandedType: 0, 24 | name: "Triangle", 25 | nameIsFixed: false, 26 | resizingConstraint: 63, 27 | resizingType: 0, 28 | rotation: 0, 29 | shouldBreakMaskChain: false, 30 | exportOptions: { 31 | _class: "exportOptions", 32 | includedLayerIds: [], 33 | layerOptions: 0, 34 | shouldTrim: false, 35 | exportFormats: [], 36 | }, 37 | frame: { 38 | _class: "rect", 39 | constrainProportions: true, 40 | height: 127, 41 | width: 127, 42 | x: 68, 43 | y: 61, 44 | }, 45 | clippingMaskMode: 0, 46 | hasClippingMask: false, 47 | style: { 48 | _class: "style", 49 | do_objectID: "ABC0F641-404C-4179-8EE2-36ADF496FB84", 50 | endMarkerType: 0, 51 | miterLimit: 10, 52 | startMarkerType: 0, 53 | windingRule: 1, 54 | blur: { 55 | _class: "blur", 56 | isEnabled: false, 57 | center: "{0.5, 0.5}", 58 | motionAngle: 0, 59 | radius: 10, 60 | saturation: 1, 61 | type: 0, 62 | }, 63 | borderOptions: { 64 | _class: "borderOptions", 65 | isEnabled: true, 66 | dashPattern: [], 67 | lineCapStyle: 0, 68 | lineJoinStyle: 0, 69 | }, 70 | borders: [ 71 | { 72 | _class: "border", 73 | isEnabled: true, 74 | fillType: 0, 75 | color: { 76 | _class: "color", 77 | alpha: 1, 78 | blue: 0.592, 79 | green: 0.592, 80 | red: 0.592, 81 | }, 82 | contextSettings: { 83 | _class: "graphicsContextSettings", 84 | blendMode: 0, 85 | opacity: 1, 86 | }, 87 | gradient: { 88 | _class: "gradient", 89 | elipseLength: 0, 90 | from: "{0.5, 0}", 91 | gradientType: 0, 92 | to: "{0.5, 1}", 93 | stops: [ 94 | { 95 | _class: "gradientStop", 96 | position: 0, 97 | color: { 98 | _class: "color", 99 | alpha: 1, 100 | blue: 1, 101 | green: 1, 102 | red: 1, 103 | }, 104 | }, 105 | { 106 | _class: "gradientStop", 107 | position: 1, 108 | color: { 109 | _class: "color", 110 | alpha: 1, 111 | blue: 0, 112 | green: 0, 113 | red: 0, 114 | }, 115 | }, 116 | ], 117 | }, 118 | position: 0, 119 | thickness: 1, 120 | }, 121 | ], 122 | colorControls: { 123 | _class: "colorControls", 124 | isEnabled: false, 125 | brightness: 0, 126 | contrast: 1, 127 | hue: 0, 128 | saturation: 1, 129 | }, 130 | contextSettings: { 131 | _class: "graphicsContextSettings", 132 | blendMode: 0, 133 | opacity: 1, 134 | }, 135 | fills: [ 136 | { 137 | _class: "fill", 138 | isEnabled: true, 139 | fillType: 0, 140 | color: { 141 | _class: "color", 142 | alpha: 1, 143 | blue: 0.847, 144 | green: 0.847, 145 | red: 0.847, 146 | }, 147 | contextSettings: { 148 | _class: "graphicsContextSettings", 149 | blendMode: 0, 150 | opacity: 1, 151 | }, 152 | gradient: { 153 | _class: "gradient", 154 | elipseLength: 0, 155 | from: "{0.5, 0}", 156 | gradientType: 0, 157 | to: "{0.5, 1}", 158 | stops: [ 159 | { 160 | _class: "gradientStop", 161 | position: 0, 162 | color: { 163 | _class: "color", 164 | alpha: 1, 165 | blue: 1, 166 | green: 1, 167 | red: 1, 168 | }, 169 | }, 170 | { 171 | _class: "gradientStop", 172 | position: 1, 173 | color: { 174 | _class: "color", 175 | alpha: 1, 176 | blue: 0, 177 | green: 0, 178 | red: 0, 179 | }, 180 | }, 181 | ], 182 | }, 183 | noiseIndex: 0, 184 | noiseIntensity: 0, 185 | patternFillType: 1, 186 | patternTileScale: 1, 187 | }, 188 | ], 189 | innerShadows: [], 190 | shadows: [], 191 | }, 192 | edited: false, 193 | isClosed: true, 194 | pointRadiusBehaviour: 1, 195 | points: [ 196 | { 197 | _class: "curvePoint", 198 | cornerRadius: 0, 199 | curveFrom: "{0.5, 0}", 200 | curveMode: 1, 201 | curveTo: "{0.5, 0}", 202 | hasCurveFrom: false, 203 | hasCurveTo: false, 204 | point: "{0.5, 0}", 205 | }, 206 | { 207 | _class: "curvePoint", 208 | cornerRadius: 0, 209 | curveFrom: "{1, 1}", 210 | curveMode: 1, 211 | curveTo: "{1, 1}", 212 | hasCurveFrom: false, 213 | hasCurveTo: false, 214 | point: "{1, 1}", 215 | }, 216 | { 217 | _class: "curvePoint", 218 | cornerRadius: 0, 219 | curveFrom: "{0, 1}", 220 | curveMode: 1, 221 | curveTo: "{0, 1}", 222 | hasCurveFrom: false, 223 | hasCurveTo: false, 224 | point: "{0, 1}", 225 | }, 226 | ], 227 | isEquilateral: false, 228 | }, 229 | ], 230 | }, 231 | } as any); 232 | 233 | const newPage = new Page({ layers: [newArtboard.toSketchJSON()] } as any); 234 | 235 | const newPack = new JSONPack({ pages: [newPage] } as any); 236 | 237 | (async () => { 238 | await newPack.write(`${DEMO_PATH}/files`); 239 | await newPack.zip(`${DEMO_PATH}/result.sketch`); 240 | })(); 241 | -------------------------------------------------------------------------------- /demo/artboard/symbol2artboard.ts: -------------------------------------------------------------------------------- 1 | import { JSONPack, Layer, Page, SketchFile, SketchType } from "../../src"; 2 | 3 | const DEMO_SOURCE_FILE = "demo/files/LayerSymbolArtboard.sketch"; 4 | const DEMO_PATH = "temp/artboard/symbol2artboard"; 5 | 6 | (async () => { 7 | const originSketch = new SketchFile(DEMO_SOURCE_FILE); 8 | const originFilePath = `${DEMO_PATH}/origin`; 9 | await originSketch.unzip(originFilePath); 10 | 11 | if (!JSONPack.isValidStructure(originFilePath)) return; 12 | 13 | const originPack = await JSONPack.fromPath(originFilePath); 14 | 15 | const allArtboardLikes = originPack.getAllArtboards(); 16 | 17 | const newLayers = allArtboardLikes.map((layer) => { 18 | if (layer._class === "symbolMaster") { 19 | const outerArtboard = new Layer({ class: "Artboard", data: null }); 20 | outerArtboard.updateProps({ 21 | data: { 22 | name: layer.name, 23 | frame: layer.frame, 24 | layers: layer.layers, 25 | } as SketchType.Artboard, 26 | }); 27 | return outerArtboard.toSketchJSON(); 28 | } 29 | 30 | return layer; 31 | }); 32 | 33 | const newPage = new Page({ layers: newLayers } as any); 34 | const newPack = new JSONPack({ pages: [newPage] } as any); 35 | 36 | await newPack.write(`${DEMO_PATH}/files`); 37 | await newPack.zip(`${DEMO_PATH}/result.sketch`); 38 | })(); 39 | -------------------------------------------------------------------------------- /demo/files/1Symbol1Artboard.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ant-design/sketch-json-api/5d38a1fdaeaa1d0af94fb4ad390f327ca6b7dfe5/demo/files/1Symbol1Artboard.sketch -------------------------------------------------------------------------------- /demo/files/CheckboxAndRadio.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ant-design/sketch-json-api/5d38a1fdaeaa1d0af94fb4ad390f327ca6b7dfe5/demo/files/CheckboxAndRadio.sketch -------------------------------------------------------------------------------- /demo/files/LayerSymbolArtboard.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ant-design/sketch-json-api/5d38a1fdaeaa1d0af94fb4ad390f327ca6b7dfe5/demo/files/LayerSymbolArtboard.sketch -------------------------------------------------------------------------------- /demo/files/SimpleButton.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ant-design/sketch-json-api/5d38a1fdaeaa1d0af94fb4ad390f327ca6b7dfe5/demo/files/SimpleButton.sketch -------------------------------------------------------------------------------- /demo/files/SomeArtboards/document.json: -------------------------------------------------------------------------------- 1 | {"_class":"document","do_objectID":"C2C31772-2A15-499A-81F1-BC4333525012","agreedToFontEmbedding":false,"autoEmbedFonts":false,"colorSpace":0,"currentPageIndex":0,"assets":{"_class":"assetCollection","do_objectID":"838EA183-1E56-4435-8C87-0FCE85CC6CAA","images":[],"colorAssets":[],"exportPresets":[],"gradientAssets":[],"imageCollection":{"_class":"imageCollection","images":{}},"colors":[],"gradients":[]},"fontReferences":[],"foreignLayerStyles":[],"foreignSwatches":[],"foreignSymbols":[],"foreignTextStyles":[],"layerStyles":{"_class":"sharedStyleContainer","objects":[]},"layerSymbols":{"_class":"symbolContainer","objects":[]},"layerTextStyles":{"_class":"sharedTextStyleContainer","objects":[]},"pages":[{"_class":"MSJSONFileReference","_ref_class":"MSImmutablePage","_ref":"pages\/A76B144E-7469-4ECF-B8D4-6293FA873AE7"}],"sharedSwatches":{"_class":"swatchContainer","objects":[]}} -------------------------------------------------------------------------------- /demo/files/SomeArtboards/meta.json: -------------------------------------------------------------------------------- 1 | {"commit":"654dd85ce95bf3ca0f27267f2e4c978a9ace95b8","pagesAndArtboards":{"A76B144E-7469-4ECF-B8D4-6293FA873AE7":{"name":"Page 1","artboards":{"621CBE87-E527-40B1-9A83-8B71F4E52C07":{"name":"SomeArtboard"},"CB03EE53-D6D2-4764-A8A6-098FEE533671":{"name":"SomeSymbolMaster"}}}},"version":130,"fonts":[],"compatibilityVersion":99,"app":"com.bohemiancoding.sketch3","autosaved":0,"variant":"NONAPPSTORE","created":{"commit":"654dd85ce95bf3ca0f27267f2e4c978a9ace95b8","appVersion":"67.2","build":100443,"app":"com.bohemiancoding.sketch3","compatibilityVersion":99,"version":130,"variant":"NONAPPSTORE"},"saveHistory":["NONAPPSTORE.100443"],"appVersion":"67.2","build":100443} -------------------------------------------------------------------------------- /demo/files/SomeArtboards/pages/A76B144E-7469-4ECF-B8D4-6293FA873AE7.json: -------------------------------------------------------------------------------- 1 | {"_class":"page","do_objectID":"A76B144E-7469-4ECF-B8D4-6293FA873AE7","booleanOperation":-1,"isFixedToViewport":false,"isFlippedHorizontal":false,"isFlippedVertical":false,"isLocked":false,"isVisible":true,"layerListExpandedType":0,"name":"Page 1","nameIsFixed":false,"resizingConstraint":63,"resizingType":0,"rotation":0,"shouldBreakMaskChain":false,"exportOptions":{"_class":"exportOptions","includedLayerIds":[],"layerOptions":0,"shouldTrim":false,"exportFormats":[]},"frame":{"_class":"rect","constrainProportions":true,"height":0,"width":0,"x":0,"y":0},"clippingMaskMode":0,"hasClippingMask":false,"style":{"_class":"style","do_objectID":"31CB91FC-A06C-4093-BDFF-5FEF689CC9FD","endMarkerType":0,"miterLimit":10,"startMarkerType":0,"windingRule":1,"blur":{"_class":"blur","isEnabled":false,"center":"{0.5, 0.5}","motionAngle":0,"radius":10,"saturation":1,"type":0},"borderOptions":{"_class":"borderOptions","isEnabled":true,"dashPattern":[],"lineCapStyle":0,"lineJoinStyle":0},"borders":[],"colorControls":{"_class":"colorControls","isEnabled":false,"brightness":0,"contrast":1,"hue":0,"saturation":1},"contextSettings":{"_class":"graphicsContextSettings","blendMode":0,"opacity":1},"fills":[],"innerShadows":[],"shadows":[]},"hasClickThrough":true,"groupLayout":{"_class":"MSImmutableFreeformGroupLayout"},"layers":[{"_class":"symbolMaster","do_objectID":"CB03EE53-D6D2-4764-A8A6-098FEE533671","booleanOperation":-1,"isFixedToViewport":false,"isFlippedHorizontal":false,"isFlippedVertical":false,"isLocked":false,"isVisible":true,"layerListExpandedType":1,"name":"SomeSymbolMaster","nameIsFixed":true,"resizingConstraint":63,"resizingType":0,"rotation":0,"shouldBreakMaskChain":true,"exportOptions":{"_class":"exportOptions","includedLayerIds":[],"layerOptions":0,"shouldTrim":false,"exportFormats":[]},"frame":{"_class":"rect","constrainProportions":false,"height":223,"width":263,"x":82,"y":79},"clippingMaskMode":0,"hasClippingMask":false,"style":{"_class":"style","do_objectID":"F863B1DC-8328-4223-85D9-D40A79701B98","endMarkerType":0,"miterLimit":10,"startMarkerType":0,"windingRule":1,"blur":{"_class":"blur","isEnabled":false,"center":"{0.5, 0.5}","motionAngle":0,"radius":10,"saturation":1,"type":0},"borderOptions":{"_class":"borderOptions","isEnabled":true,"dashPattern":[],"lineCapStyle":0,"lineJoinStyle":0},"borders":[],"colorControls":{"_class":"colorControls","isEnabled":false,"brightness":0,"contrast":1,"hue":0,"saturation":1},"contextSettings":{"_class":"graphicsContextSettings","blendMode":0,"opacity":1},"fills":[],"innerShadows":[],"shadows":[]},"hasClickThrough":true,"groupLayout":{"_class":"MSImmutableFreeformGroupLayout"},"layers":[{"_class":"star","do_objectID":"9EC8EB21-BE6A-4405-BE01-DC911EE05609","booleanOperation":-1,"isFixedToViewport":false,"isFlippedHorizontal":false,"isFlippedVertical":false,"isLocked":false,"isVisible":true,"layerListExpandedType":0,"name":"Star","nameIsFixed":false,"resizingConstraint":63,"resizingType":0,"rotation":0,"shouldBreakMaskChain":false,"exportOptions":{"_class":"exportOptions","includedLayerIds":[],"layerOptions":0,"shouldTrim":false,"exportFormats":[]},"frame":{"_class":"rect","constrainProportions":false,"height":133,"width":174,"x":45,"y":45},"clippingMaskMode":0,"hasClippingMask":false,"style":{"_class":"style","do_objectID":"28FDB861-788D-4051-89DA-D66F4D192F7D","endMarkerType":0,"miterLimit":10,"startMarkerType":0,"windingRule":1,"blur":{"_class":"blur","isEnabled":false,"center":"{0.5, 0.5}","motionAngle":0,"radius":10,"saturation":1,"type":0},"borderOptions":{"_class":"borderOptions","isEnabled":true,"dashPattern":[],"lineCapStyle":0,"lineJoinStyle":0},"borders":[{"_class":"border","isEnabled":true,"fillType":0,"color":{"_class":"color","alpha":1,"blue":0.592,"green":0.592,"red":0.592},"contextSettings":{"_class":"graphicsContextSettings","blendMode":0,"opacity":1},"gradient":{"_class":"gradient","elipseLength":0,"from":"{0.5, 0}","gradientType":0,"to":"{0.5, 1}","stops":[{"_class":"gradientStop","position":0,"color":{"_class":"color","alpha":1,"blue":1,"green":1,"red":1}},{"_class":"gradientStop","position":1,"color":{"_class":"color","alpha":1,"blue":0,"green":0,"red":0}}]},"position":0,"thickness":1}],"colorControls":{"_class":"colorControls","isEnabled":false,"brightness":0,"contrast":1,"hue":0,"saturation":1},"contextSettings":{"_class":"graphicsContextSettings","blendMode":0,"opacity":1},"fills":[{"_class":"fill","isEnabled":true,"fillType":0,"color":{"_class":"color","alpha":1,"blue":0.847,"green":0.847,"red":0.847},"contextSettings":{"_class":"graphicsContextSettings","blendMode":0,"opacity":1},"gradient":{"_class":"gradient","elipseLength":0,"from":"{0.5, 0}","gradientType":0,"to":"{0.5, 1}","stops":[{"_class":"gradientStop","position":0,"color":{"_class":"color","alpha":1,"blue":1,"green":1,"red":1}},{"_class":"gradientStop","position":1,"color":{"_class":"color","alpha":1,"blue":0,"green":0,"red":0}}]},"noiseIndex":0,"noiseIntensity":0,"patternFillType":1,"patternTileScale":1}],"innerShadows":[],"shadows":[]},"edited":false,"isClosed":true,"pointRadiusBehaviour":1,"points":[{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0.5, 0.75}","curveMode":1,"curveTo":"{0.5, 0.75}","hasCurveFrom":false,"hasCurveTo":false,"point":"{0.5, 0.75}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0.20610737385376349, 0.90450849718747373}","curveMode":1,"curveTo":"{0.20610737385376349, 0.90450849718747373}","hasCurveFrom":false,"hasCurveTo":false,"point":"{0.20610737385376349, 0.90450849718747373}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0.26223587092621159, 0.57725424859373686}","curveMode":1,"curveTo":"{0.26223587092621159, 0.57725424859373686}","hasCurveFrom":false,"hasCurveTo":false,"point":"{0.26223587092621159, 0.57725424859373686}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0.024471741852423179, 0.34549150281252639}","curveMode":1,"curveTo":"{0.024471741852423179, 0.34549150281252639}","hasCurveFrom":false,"hasCurveTo":false,"point":"{0.024471741852423179, 0.34549150281252639}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0.35305368692688166, 0.29774575140626314}","curveMode":1,"curveTo":"{0.35305368692688166, 0.29774575140626314}","hasCurveFrom":false,"hasCurveTo":false,"point":"{0.35305368692688166, 0.29774575140626314}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0.49999999999999989, 0}","curveMode":1,"curveTo":"{0.49999999999999989, 0}","hasCurveFrom":false,"hasCurveTo":false,"point":"{0.49999999999999989, 0}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0.64694631307311823, 0.29774575140626314}","curveMode":1,"curveTo":"{0.64694631307311823, 0.29774575140626314}","hasCurveFrom":false,"hasCurveTo":false,"point":"{0.64694631307311823, 0.29774575140626314}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0.97552825814757682, 0.34549150281252616}","curveMode":1,"curveTo":"{0.97552825814757682, 0.34549150281252616}","hasCurveFrom":false,"hasCurveTo":false,"point":"{0.97552825814757682, 0.34549150281252616}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0.73776412907378841, 0.57725424859373675}","curveMode":1,"curveTo":"{0.73776412907378841, 0.57725424859373675}","hasCurveFrom":false,"hasCurveTo":false,"point":"{0.73776412907378841, 0.57725424859373675}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0.79389262614623668, 0.90450849718747361}","curveMode":1,"curveTo":"{0.79389262614623668, 0.90450849718747361}","hasCurveFrom":false,"hasCurveTo":false,"point":"{0.79389262614623668, 0.90450849718747361}"}],"numberOfPoints":5,"radius":0.5}],"hasBackgroundColor":false,"includeBackgroundColorInExport":true,"includeInCloudUpload":true,"isFlowHome":false,"presetDictionary":{},"resizesContent":false,"backgroundColor":{"_class":"color","alpha":1,"blue":1,"green":1,"red":1},"horizontalRulerData":{"_class":"rulerData","base":0,"guides":[]},"verticalRulerData":{"_class":"rulerData","base":0,"guides":[]},"includeBackgroundColorInInstance":true,"symbolID":"27F1B64C-8608-48C8-A7CB-CB907039D44E","changeIdentifier":13,"overrideProperties":[],"allowsOverrides":true},{"_class":"artboard","do_objectID":"621CBE87-E527-40B1-9A83-8B71F4E52C07","booleanOperation":-1,"isFixedToViewport":false,"isFlippedHorizontal":false,"isFlippedVertical":false,"isLocked":false,"isVisible":true,"layerListExpandedType":1,"name":"SomeArtboard","nameIsFixed":true,"resizingConstraint":63,"resizingType":0,"rotation":0,"shouldBreakMaskChain":true,"exportOptions":{"_class":"exportOptions","includedLayerIds":[],"layerOptions":0,"shouldTrim":false,"exportFormats":[]},"frame":{"_class":"rect","constrainProportions":false,"height":223,"width":282,"x":438,"y":79},"clippingMaskMode":0,"hasClippingMask":false,"style":{"_class":"style","do_objectID":"6E4E9A1F-3CB4-4EA9-8911-F7131444EA4E","endMarkerType":0,"miterLimit":10,"startMarkerType":0,"windingRule":1,"blur":{"_class":"blur","isEnabled":false,"center":"{0.5, 0.5}","motionAngle":0,"radius":10,"saturation":1,"type":0},"borderOptions":{"_class":"borderOptions","isEnabled":true,"dashPattern":[],"lineCapStyle":0,"lineJoinStyle":0},"borders":[],"colorControls":{"_class":"colorControls","isEnabled":false,"brightness":0,"contrast":1,"hue":0,"saturation":1},"contextSettings":{"_class":"graphicsContextSettings","blendMode":0,"opacity":1},"fills":[],"innerShadows":[],"shadows":[]},"hasClickThrough":true,"groupLayout":{"_class":"MSImmutableFreeformGroupLayout"},"layers":[{"_class":"oval","do_objectID":"65BBED8B-F8D3-42FA-A94F-1515E1449798","booleanOperation":-1,"isFixedToViewport":false,"isFlippedHorizontal":false,"isFlippedVertical":false,"isLocked":false,"isVisible":true,"layerListExpandedType":0,"name":"Oval","nameIsFixed":false,"resizingConstraint":63,"resizingType":0,"rotation":0,"shouldBreakMaskChain":false,"exportOptions":{"_class":"exportOptions","includedLayerIds":[],"layerOptions":0,"shouldTrim":false,"exportFormats":[]},"frame":{"_class":"rect","constrainProportions":false,"height":124,"width":145,"x":70,"y":45},"clippingMaskMode":0,"hasClippingMask":false,"style":{"_class":"style","do_objectID":"FB1EC35D-320E-40F6-8FB3-18C059A121A4","endMarkerType":0,"miterLimit":10,"startMarkerType":0,"windingRule":1,"blur":{"_class":"blur","isEnabled":false,"center":"{0.5, 0.5}","motionAngle":0,"radius":10,"saturation":1,"type":0},"borderOptions":{"_class":"borderOptions","isEnabled":true,"dashPattern":[],"lineCapStyle":0,"lineJoinStyle":0},"borders":[{"_class":"border","isEnabled":true,"fillType":0,"color":{"_class":"color","alpha":1,"blue":0.592,"green":0.592,"red":0.592},"contextSettings":{"_class":"graphicsContextSettings","blendMode":0,"opacity":1},"gradient":{"_class":"gradient","elipseLength":0,"from":"{0.5, 0}","gradientType":0,"to":"{0.5, 1}","stops":[{"_class":"gradientStop","position":0,"color":{"_class":"color","alpha":1,"blue":1,"green":1,"red":1}},{"_class":"gradientStop","position":1,"color":{"_class":"color","alpha":1,"blue":0,"green":0,"red":0}}]},"position":0,"thickness":1}],"colorControls":{"_class":"colorControls","isEnabled":false,"brightness":0,"contrast":1,"hue":0,"saturation":1},"contextSettings":{"_class":"graphicsContextSettings","blendMode":0,"opacity":1},"fills":[{"_class":"fill","isEnabled":true,"fillType":0,"color":{"_class":"color","alpha":1,"blue":0.847,"green":0.847,"red":0.847},"contextSettings":{"_class":"graphicsContextSettings","blendMode":0,"opacity":1},"gradient":{"_class":"gradient","elipseLength":0,"from":"{0.5, 0}","gradientType":0,"to":"{0.5, 1}","stops":[{"_class":"gradientStop","position":0,"color":{"_class":"color","alpha":1,"blue":1,"green":1,"red":1}},{"_class":"gradientStop","position":1,"color":{"_class":"color","alpha":1,"blue":0,"green":0,"red":0}}]},"noiseIndex":0,"noiseIntensity":0,"patternFillType":1,"patternTileScale":1}],"innerShadows":[],"shadows":[]},"edited":false,"isClosed":true,"pointRadiusBehaviour":1,"points":[{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0.77614237490000004, 1}","curveMode":2,"curveTo":"{0.22385762510000001, 1}","hasCurveFrom":true,"hasCurveTo":true,"point":"{0.5, 1}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{1, 0.22385762510000001}","curveMode":2,"curveTo":"{1, 0.77614237490000004}","hasCurveFrom":true,"hasCurveTo":true,"point":"{1, 0.5}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0.22385762510000001, 0}","curveMode":2,"curveTo":"{0.77614237490000004, 0}","hasCurveFrom":true,"hasCurveTo":true,"point":"{0.5, 0}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0, 0.77614237490000004}","curveMode":2,"curveTo":"{0, 0.22385762510000001}","hasCurveFrom":true,"hasCurveTo":true,"point":"{0, 0.5}"}]}],"hasBackgroundColor":false,"includeBackgroundColorInExport":true,"includeInCloudUpload":true,"isFlowHome":false,"presetDictionary":{},"resizesContent":false,"backgroundColor":{"_class":"color","alpha":1,"blue":1,"green":1,"red":1},"horizontalRulerData":{"_class":"rulerData","base":0,"guides":[]},"verticalRulerData":{"_class":"rulerData","base":0,"guides":[]}},{"_class":"triangle","do_objectID":"0CA01ED5-EB2F-4976-92BA-BE76A878A689","booleanOperation":-1,"isFixedToViewport":false,"isFlippedHorizontal":false,"isFlippedVertical":false,"isLocked":false,"isVisible":true,"layerListExpandedType":0,"name":"TriangleLayer","nameIsFixed":true,"resizingConstraint":63,"resizingType":0,"rotation":0,"shouldBreakMaskChain":false,"exportOptions":{"_class":"exportOptions","includedLayerIds":[],"layerOptions":0,"shouldTrim":false,"exportFormats":[]},"frame":{"_class":"rect","constrainProportions":false,"height":178,"width":265,"x":270,"y":427},"clippingMaskMode":0,"hasClippingMask":false,"style":{"_class":"style","do_objectID":"BCC877D1-7605-497F-B6D7-F2D799BC9840","endMarkerType":0,"miterLimit":10,"startMarkerType":0,"windingRule":1,"blur":{"_class":"blur","isEnabled":false,"center":"{0.5, 0.5}","motionAngle":0,"radius":10,"saturation":1,"type":0},"borderOptions":{"_class":"borderOptions","isEnabled":true,"dashPattern":[],"lineCapStyle":0,"lineJoinStyle":0},"borders":[{"_class":"border","isEnabled":true,"fillType":0,"color":{"_class":"color","alpha":1,"blue":0.592,"green":0.592,"red":0.592},"contextSettings":{"_class":"graphicsContextSettings","blendMode":0,"opacity":1},"gradient":{"_class":"gradient","elipseLength":0,"from":"{0.5, 0}","gradientType":0,"to":"{0.5, 1}","stops":[{"_class":"gradientStop","position":0,"color":{"_class":"color","alpha":1,"blue":1,"green":1,"red":1}},{"_class":"gradientStop","position":1,"color":{"_class":"color","alpha":1,"blue":0,"green":0,"red":0}}]},"position":0,"thickness":1}],"colorControls":{"_class":"colorControls","isEnabled":false,"brightness":0,"contrast":1,"hue":0,"saturation":1},"contextSettings":{"_class":"graphicsContextSettings","blendMode":0,"opacity":1},"fills":[{"_class":"fill","isEnabled":true,"fillType":0,"color":{"_class":"color","alpha":1,"blue":0.847,"green":0.847,"red":0.847},"contextSettings":{"_class":"graphicsContextSettings","blendMode":0,"opacity":1},"gradient":{"_class":"gradient","elipseLength":0,"from":"{0.5, 0}","gradientType":0,"to":"{0.5, 1}","stops":[{"_class":"gradientStop","position":0,"color":{"_class":"color","alpha":1,"blue":1,"green":1,"red":1}},{"_class":"gradientStop","position":1,"color":{"_class":"color","alpha":1,"blue":0,"green":0,"red":0}}]},"noiseIndex":0,"noiseIntensity":0,"patternFillType":1,"patternTileScale":1}],"innerShadows":[],"shadows":[]},"edited":false,"isClosed":true,"pointRadiusBehaviour":1,"points":[{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0.5, 0}","curveMode":1,"curveTo":"{0.5, 0}","hasCurveFrom":false,"hasCurveTo":false,"point":"{0.5, 0}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{1, 1}","curveMode":1,"curveTo":"{1, 1}","hasCurveFrom":false,"hasCurveTo":false,"point":"{1, 1}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0, 1}","curveMode":1,"curveTo":"{0, 1}","hasCurveFrom":false,"hasCurveTo":false,"point":"{0, 1}"}],"isEquilateral":false}],"includeInCloudUpload":true,"horizontalRulerData":{"_class":"rulerData","base":0,"guides":[]},"verticalRulerData":{"_class":"rulerData","base":0,"guides":[]}} -------------------------------------------------------------------------------- /demo/files/SomeArtboards/previews/preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ant-design/sketch-json-api/5d38a1fdaeaa1d0af94fb4ad390f327ca6b7dfe5/demo/files/SomeArtboards/previews/preview.png -------------------------------------------------------------------------------- /demo/files/SomeArtboards/user.json: -------------------------------------------------------------------------------- 1 | {"document":{"pageListHeight":85,"componentSidebarTreeStructure":1,"libraryListCollapsed":0,"pageListCollapsed":0},"A76B144E-7469-4ECF-B8D4-6293FA873AE7":{"scrollOrigin":"{0, 2}","zoomValue":1}} -------------------------------------------------------------------------------- /demo/files/TechUIdemo.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ant-design/sketch-json-api/5d38a1fdaeaa1d0af94fb4ad390f327ca6b7dfe5/demo/files/TechUIdemo.sketch -------------------------------------------------------------------------------- /demo/files/bitmap.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ant-design/sketch-json-api/5d38a1fdaeaa1d0af94fb4ad390f327ca6b7dfe5/demo/files/bitmap.sketch -------------------------------------------------------------------------------- /demo/files/miniSamplePack/document.json: -------------------------------------------------------------------------------- 1 | {"_class":"document","do_objectID":"99077D84-8B3D-47EA-8AC0-9BAE0DA67330","agreedToFontEmbedding":false,"autoEmbedFonts":false,"colorSpace":0,"currentPageIndex":0,"assets":{"_class":"assetCollection","do_objectID":"4B119092-7CB3-4EEC-A4C1-E2A2131595A9","images":[],"colorAssets":[],"exportPresets":[],"gradientAssets":[],"imageCollection":{"_class":"imageCollection","images":{}},"colors":[],"gradients":[]},"fontReferences":[],"foreignLayerStyles":[],"foreignSwatches":[],"foreignSymbols":[],"foreignTextStyles":[],"layerStyles":{"_class":"sharedStyleContainer","objects":[]},"layerSymbols":{"_class":"symbolContainer","objects":[]},"layerTextStyles":{"_class":"sharedTextStyleContainer","objects":[]},"pages":[{"_class":"MSJSONFileReference","_ref_class":"MSImmutablePage","_ref":"pages\/990FFC96-8159-4E45-9A1F-6E5A25D57C8F"}],"sharedSwatches":{"_class":"swatchContainer","objects":[]}} -------------------------------------------------------------------------------- /demo/files/miniSamplePack/meta.json: -------------------------------------------------------------------------------- 1 | {"commit":"1214a6256faf6591863918c936f38c9a7f2124bd","pagesAndArtboards":{"990FFC96-8159-4E45-9A1F-6E5A25D57C8F":{"name":"Page 1","artboards":{}}},"version":130,"fonts":[],"compatibilityVersion":99,"app":"com.bohemiancoding.sketch3","autosaved":0,"variant":"NONAPPSTORE","created":{"commit":"1214a6256faf6591863918c936f38c9a7f2124bd","appVersion":"66","build":97063,"app":"com.bohemiancoding.sketch3","compatibilityVersion":99,"version":130,"variant":"NONAPPSTORE"},"saveHistory":["NONAPPSTORE.97063"],"appVersion":"66","build":97063} -------------------------------------------------------------------------------- /demo/files/miniSamplePack/pages/990FFC96-8159-4E45-9A1F-6E5A25D57C8F.json: -------------------------------------------------------------------------------- 1 | {"_class":"page","do_objectID":"990FFC96-8159-4E45-9A1F-6E5A25D57C8F","booleanOperation":-1,"isFixedToViewport":false,"isFlippedHorizontal":false,"isFlippedVertical":false,"isLocked":false,"isVisible":true,"layerListExpandedType":0,"name":"Page 1","nameIsFixed":false,"resizingConstraint":63,"resizingType":0,"rotation":0,"shouldBreakMaskChain":false,"exportOptions":{"_class":"exportOptions","includedLayerIds":[],"layerOptions":0,"shouldTrim":false,"exportFormats":[]},"frame":{"_class":"rect","constrainProportions":true,"height":0,"width":0,"x":0,"y":0},"clippingMaskMode":0,"hasClippingMask":false,"style":{"_class":"style","do_objectID":"644A42FE-0580-4327-801B-52870CE7208B","endMarkerType":0,"miterLimit":10,"startMarkerType":0,"windingRule":1,"blur":{"_class":"blur","isEnabled":false,"center":"{0.5, 0.5}","motionAngle":0,"radius":10,"saturation":1,"type":0},"borderOptions":{"_class":"borderOptions","isEnabled":true,"dashPattern":[],"lineCapStyle":0,"lineJoinStyle":0},"borders":[],"colorControls":{"_class":"colorControls","isEnabled":false,"brightness":0,"contrast":1,"hue":0,"saturation":1},"contextSettings":{"_class":"graphicsContextSettings","blendMode":0,"opacity":1},"fills":[],"innerShadows":[],"shadows":[]},"hasClickThrough":true,"groupLayout":{"_class":"MSImmutableFreeformGroupLayout"},"layers":[{"_class":"rectangle","do_objectID":"80621615-46BB-412D-80DD-18F035C4CC5B","booleanOperation":-1,"isFixedToViewport":false,"isFlippedHorizontal":false,"isFlippedVertical":false,"isLocked":false,"isVisible":true,"layerListExpandedType":0,"name":"Rectangle","nameIsFixed":false,"resizingConstraint":63,"resizingType":0,"rotation":0,"shouldBreakMaskChain":false,"exportOptions":{"_class":"exportOptions","includedLayerIds":[],"layerOptions":0,"shouldTrim":false,"exportFormats":[]},"frame":{"_class":"rect","constrainProportions":true,"height":86,"width":86,"x":105,"y":95},"clippingMaskMode":0,"hasClippingMask":false,"style":{"_class":"style","do_objectID":"A8CA9C32-4284-4705-93BA-0A041D802DBB","endMarkerType":0,"miterLimit":10,"startMarkerType":0,"windingRule":1,"blur":{"_class":"blur","isEnabled":false,"center":"{0.5, 0.5}","motionAngle":0,"radius":10,"saturation":1,"type":0},"borderOptions":{"_class":"borderOptions","isEnabled":true,"dashPattern":[],"lineCapStyle":0,"lineJoinStyle":0},"borders":[{"_class":"border","isEnabled":true,"fillType":0,"color":{"_class":"color","alpha":1,"blue":0.592,"green":0.592,"red":0.592},"contextSettings":{"_class":"graphicsContextSettings","blendMode":0,"opacity":1},"gradient":{"_class":"gradient","elipseLength":0,"from":"{0.5, 0}","gradientType":0,"to":"{0.5, 1}","stops":[{"_class":"gradientStop","position":0,"color":{"_class":"color","alpha":1,"blue":1,"green":1,"red":1}},{"_class":"gradientStop","position":1,"color":{"_class":"color","alpha":1,"blue":0,"green":0,"red":0}}]},"position":0,"thickness":1}],"colorControls":{"_class":"colorControls","isEnabled":false,"brightness":0,"contrast":1,"hue":0,"saturation":1},"contextSettings":{"_class":"graphicsContextSettings","blendMode":0,"opacity":1},"fills":[{"_class":"fill","isEnabled":true,"fillType":0,"color":{"_class":"color","alpha":1,"blue":0.847,"green":0.847,"red":0.847},"contextSettings":{"_class":"graphicsContextSettings","blendMode":0,"opacity":1},"gradient":{"_class":"gradient","elipseLength":0,"from":"{0.5, 0}","gradientType":0,"to":"{0.5, 1}","stops":[{"_class":"gradientStop","position":0,"color":{"_class":"color","alpha":1,"blue":1,"green":1,"red":1}},{"_class":"gradientStop","position":1,"color":{"_class":"color","alpha":1,"blue":0,"green":0,"red":0}}]},"noiseIndex":0,"noiseIntensity":0,"patternFillType":1,"patternTileScale":1}],"innerShadows":[],"shadows":[]},"edited":false,"isClosed":true,"pointRadiusBehaviour":1,"points":[{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0, 0}","curveMode":1,"curveTo":"{0, 0}","hasCurveFrom":false,"hasCurveTo":false,"point":"{0, 0}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{1, 0}","curveMode":1,"curveTo":"{1, 0}","hasCurveFrom":false,"hasCurveTo":false,"point":"{1, 0}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{1, 1}","curveMode":1,"curveTo":"{1, 1}","hasCurveFrom":false,"hasCurveTo":false,"point":"{1, 1}"},{"_class":"curvePoint","cornerRadius":0,"curveFrom":"{0, 1}","curveMode":1,"curveTo":"{0, 1}","hasCurveFrom":false,"hasCurveTo":false,"point":"{0, 1}"}],"fixedRadius":0,"needsConvertionToNewRoundCorners":false,"hasConvertedToNewRoundCorners":true}],"includeInCloudUpload":true,"horizontalRulerData":{"_class":"rulerData","base":0,"guides":[]},"verticalRulerData":{"_class":"rulerData","base":0,"guides":[]}} -------------------------------------------------------------------------------- /demo/files/miniSamplePack/previews/preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ant-design/sketch-json-api/5d38a1fdaeaa1d0af94fb4ad390f327ca6b7dfe5/demo/files/miniSamplePack/previews/preview.png -------------------------------------------------------------------------------- /demo/files/miniSamplePack/user.json: -------------------------------------------------------------------------------- 1 | {"document":{"pageListHeight":85,"pageListCollapsed":0},"990FFC96-8159-4E45-9A1F-6E5A25D57C8F":{"scrollOrigin":"{0, 0}","zoomValue":1}} -------------------------------------------------------------------------------- /demo/label/label.ts: -------------------------------------------------------------------------------- 1 | import { SketchFile, JSONPack, Page, labelSymbolMaster } from "../../src"; 2 | 3 | const ASSET_META = { 4 | brand: "some design system", 5 | assets: { 6 | atoms: [ 7 | { 8 | identifier: "checkbox", 9 | name: "Checkbox", 10 | package: "@com/someds", 11 | }, 12 | { 13 | identifier: "radio", 14 | name: "Radio", 15 | package: "@com/someds", 16 | }, 17 | ], 18 | examples: [ 19 | { 20 | atomAssetID: "checkbox", 21 | identifier: "checkbox/unselected", 22 | name: "Checkbox(unselected)", 23 | }, 24 | { 25 | atomAssetID: "checkbox", 26 | identifier: "checkbox/hover", 27 | name: "Checkbox(hover)", 28 | }, 29 | { 30 | atomAssetID: "checkbox", 31 | identifier: "checkbox/selected", 32 | name: "Checkbox(selected)", 33 | }, 34 | { 35 | atomAssetID: "radio", 36 | identifier: "radio/unselected", 37 | name: "Radio(unselected)", 38 | }, 39 | { 40 | atomAssetID: "radio", 41 | identifier: "radio/hover", 42 | name: "Radio(hover)", 43 | }, 44 | { 45 | atomAssetID: "radio", 46 | identifier: "radio/selected", 47 | name: "Radio(selected)", 48 | }, 49 | ], 50 | }, 51 | }; 52 | 53 | const sampleSketchPath = "demo/files/CheckboxAndRadio.sketch"; 54 | 55 | const originSketch = new SketchFile(sampleSketchPath); 56 | 57 | const targetPath = "temp/label/files"; 58 | 59 | originSketch 60 | .unzip(targetPath) 61 | .then(() => { 62 | let jsonPack; 63 | if (JSONPack.isValidStructure(targetPath)) { 64 | jsonPack = JSONPack.fromPathSync(targetPath); 65 | 66 | const pages: Page[] = jsonPack.getPages(); 67 | 68 | pages.forEach((page) => { 69 | page.symbolMasters().forEach((symbolMaster, index) => { 70 | const labelInfo = ASSET_META.assets.examples.find( 71 | (example) => example.identifier === symbolMaster.name 72 | ); 73 | 74 | if (labelInfo) { 75 | const atom = ASSET_META.assets.atoms.find( 76 | (atom) => atom.identifier === labelInfo.atomAssetID 77 | ); 78 | 79 | const labelledSymbolMaster = labelSymbolMaster(symbolMaster, { 80 | example: labelInfo, 81 | atom, 82 | }); 83 | 84 | page.symbolMasters()[index] = labelledSymbolMaster; 85 | } 86 | }); 87 | }); 88 | 89 | return jsonPack; 90 | } 91 | }) 92 | .then((jsonPack) => { 93 | const checkPath = "temp/label/check"; 94 | jsonPack?.writeSync(checkPath); 95 | }); 96 | -------------------------------------------------------------------------------- /demo/label/techui/meta.ts: -------------------------------------------------------------------------------- 1 | export const TechUI_META = { 2 | brand: "TechUI", 3 | version: "1.22.2", 4 | package: "@alipay/tech-ui", 5 | assets: { 6 | atoms: [ 7 | { 8 | identifier: "welcomeheader", 9 | name: "WelcomeHeader", 10 | }, 11 | { 12 | identifier: "chartcard", 13 | name: "ChartCard", 14 | }, 15 | { 16 | identifier: "statsoverview", 17 | name: "StatsOverview", 18 | }, 19 | ], 20 | examples: [ 21 | { 22 | atomAssetID: "welcomeheader", 23 | identifier: "welcomeheader/steps", 24 | name: "WelcomeHeader(Steps)", 25 | layerName: 26 | "1.Overview|概览类/©1.WelcomeHeader|欢迎页头/1.Steps|多操作引导", 27 | }, 28 | { 29 | atomAssetID: "welcomeheader", 30 | identifier: "welcomeheader/recommend", 31 | name: "WelcomeHeader(Recommend)", 32 | layerName: 33 | "1.Overview|概览类/©1.WelcomeHeader|欢迎页头/2.Recommend|主操作引导", 34 | }, 35 | { 36 | atomAssetID: "chartcard", 37 | identifier: "chartcard/basic1", 38 | name: "ChartCard(Basic1)", 39 | layerName: 40 | "2.DataVis|数据可视化类/©1.ChartCard|指标卡/1.Basic1|基本使用1", 41 | }, 42 | { 43 | atomAssetID: "chartcard", 44 | identifier: "chartcard/basic2", 45 | name: "ChartCard(Basic2)", 46 | layerName: 47 | "2.DataVis|数据可视化类/©1.ChartCard|指标卡/2.Basic2|基本使用2", 48 | }, 49 | { 50 | atomAssetID: "chartcard", 51 | identifier: "chartcard/basic3", 52 | name: "ChartCard(Basic3)", 53 | layerName: 54 | "2.DataVis|数据可视化类/©1.ChartCard|指标卡/3.Basic3|基本使用3", 55 | }, 56 | { 57 | atomAssetID: "chartcard", 58 | identifier: "chartcard/basic4", 59 | name: "ChartCard(Basic4)", 60 | layerName: 61 | "2.DataVis|数据可视化类/©1.ChartCard|指标卡/4.Basic4|基本使用4", 62 | }, 63 | { 64 | atomAssetID: "chartcard", 65 | identifier: "chartcard/ratio1", 66 | name: "ChartCard(Ratio1)", 67 | layerName: 68 | "2.DataVis|数据可视化类/©1.ChartCard|指标卡/5.Ratio1|比例趋势1", 69 | }, 70 | { 71 | atomAssetID: "chartcard", 72 | identifier: "chartcard/ratio2", 73 | name: "ChartCard(Ratio2)", 74 | layerName: 75 | "2.DataVis|数据可视化类/©1.ChartCard|指标卡/6.Ratio2|比例趋势2", 76 | }, 77 | { 78 | atomAssetID: "chartcard", 79 | identifier: "chartcard/ratio3", 80 | name: "ChartCard(Ratio3)", 81 | layerName: 82 | "2.DataVis|数据可视化类/©1.ChartCard|指标卡/7.Ratio3|比例趋势3", 83 | }, 84 | { 85 | atomAssetID: "chartcard", 86 | identifier: "chartcard/titletip1", 87 | name: "ChartCard(TitleTip1)", 88 | layerName: 89 | "2.DataVis|数据可视化类/©1.ChartCard|指标卡/8.TitleTip1|标题带提示1", 90 | }, 91 | { 92 | atomAssetID: "chartcard", 93 | identifier: "chartcard/titletip2", 94 | name: "ChartCard(Titletip2)", 95 | layerName: 96 | "2.DataVis|数据可视化类/©1.ChartCard|指标卡/9.TitleTip2|标题带提示2", 97 | }, 98 | { 99 | atomAssetID: "statsoverview", 100 | identifier: "statsoverview/standard", 101 | name: "StatsOverview(Standard)", 102 | layerName: 103 | "2.DataVis|数据可视化类/©2.StatsOverview|数值统计总览/1.Standard|标准模式", 104 | }, 105 | { 106 | atomAssetID: "statsoverview", 107 | identifier: "statsoverview/standard-interactive", 108 | name: "StatsOverview(Standard-Interactive)", 109 | layerName: 110 | "2.DataVis|数据可视化类/©2.StatsOverview|数值统计总览/2.Standard-Interactive|标准模式-可交互", 111 | }, 112 | { 113 | atomAssetID: "statsoverview", 114 | identifier: "statsoverview/minimal-interactive", 115 | name: "StatsOverview(Minimal-Interactive)", 116 | layerName: 117 | "2.DataVis|数据可视化类/©2.StatsOverview|数值统计总览/3.Minimal-Interactive|简易模式-可交互", 118 | }, 119 | ], 120 | }, 121 | }; 122 | -------------------------------------------------------------------------------- /demo/label/techui/techui.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | 3 | import { SketchFile, JSONPack, Page, labelSymbolMaster } from "../../../src"; 4 | import { TechUI_META } from "./meta"; 5 | 6 | const ASSET_META = TechUI_META; 7 | const sampleSketchPath = "demo/files/TechUIdemo.sketch"; 8 | 9 | const originSketch = new SketchFile(sampleSketchPath); 10 | 11 | const targetPath = "temp/techui/files"; 12 | 13 | if (fs.existsSync(targetPath)) { 14 | //@todo: delete 15 | } 16 | 17 | originSketch 18 | .unzip(targetPath) 19 | .then(() => { 20 | let jsonPack; 21 | if (JSONPack.isValidStructure(targetPath)) { 22 | jsonPack = JSONPack.fromPathSync(targetPath); 23 | 24 | const pages: Page[] = jsonPack.getPages(); 25 | 26 | pages.forEach((page) => { 27 | page.symbolMasters().forEach((symbolMaster, index) => { 28 | const labelInfo = ASSET_META.assets.examples.find( 29 | (example) => example.layerName === symbolMaster.name 30 | ); 31 | 32 | if (labelInfo) { 33 | const atom = ASSET_META.assets.atoms.find( 34 | (atom) => atom.identifier === labelInfo.atomAssetID 35 | ); 36 | 37 | const labelledSymbolMaster = labelSymbolMaster(symbolMaster, { 38 | library: { 39 | libName: ASSET_META.brand, 40 | package: ASSET_META.package, 41 | version: ASSET_META.version, 42 | }, 43 | atom, 44 | example: labelInfo, 45 | }); 46 | 47 | page.symbolMasters()[index] = labelledSymbolMaster; 48 | } 49 | }); 50 | }); 51 | 52 | return jsonPack; 53 | } 54 | }) 55 | .then(async (jsonPack) => { 56 | if (!jsonPack) { 57 | throw new Error("JSON pack is not found."); 58 | } 59 | const checkPath = "temp/techui/labeljson"; 60 | jsonPack.writeSync(checkPath); 61 | await jsonPack.zip("temp/techui/techui-label.sketch"); 62 | }); 63 | -------------------------------------------------------------------------------- /demo/new/layout.ts: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | import { JSONPack, Page, Layer } from "../../src"; 3 | 4 | const targetPath = "temp/new/layout"; 5 | const newPackPath = path.join(targetPath, "layout"); 6 | 7 | (async () => { 8 | try { 9 | const layers = []; 10 | for (let i = 0; i < 100; i++) { 11 | const newLayer = new Layer(); 12 | newLayer.updateProps({ 13 | data: { 14 | name: `layer ${i}`, 15 | frame: { 16 | _class: "rect", 17 | constrainProportions: false, 18 | height: Math.floor(Math.random() * Math.floor(700)), 19 | width: Math.floor(Math.random() * Math.floor(400)), 20 | x: Math.floor(Math.random() * Math.floor(50)), 21 | y: Math.floor(Math.random() * Math.floor(50)), 22 | }, 23 | }, 24 | } as any); 25 | 26 | layers.push(newLayer.toSketchJSON()); 27 | } 28 | 29 | const newPage = new Page({ layers } as any); 30 | newPage.reLayoutLayers(); 31 | 32 | const newPack = new JSONPack({ pages: [newPage] } as any); 33 | 34 | await newPack.write(newPackPath); 35 | await newPack.zip(path.join(path.dirname(newPackPath), "layout.sketch")); 36 | } catch (error) { 37 | throw error; 38 | } 39 | })(); 40 | -------------------------------------------------------------------------------- /demo/new/new-from-json.ts: -------------------------------------------------------------------------------- 1 | import * as fse from "fs-extra"; 2 | import { curl } from "urllib"; 3 | import { v4 } from "uuid"; 4 | import { JSONPack, Page, Layer } from "../../src"; 5 | 6 | const downloadFile = async (url: string, dest: string): Promise => { 7 | const file = fse.createWriteStream(dest); 8 | await curl(url, { writeStream: file }); 9 | }; 10 | 11 | (async () => { 12 | await downloadFile( 13 | "http://some-oss/url.json", // replace your url 14 | "/tmp/try.json" 15 | ); 16 | 17 | const obj = await fse.readJSON("/tmp/try.json"); 18 | 19 | const urllist: any[] = []; 20 | obj?.data?.forEach((d: any) => { 21 | d.blocks?.forEach((b: any) => { 22 | const filename = b.title || v4(); 23 | const str = b.sketchData; 24 | if (str) { 25 | const info = JSON.parse(str); 26 | const url = info?.ossUrl; 27 | if (url) { 28 | urllist.push({ filename, url }); 29 | } 30 | } 31 | }); 32 | }); 33 | 34 | console.log(urllist); 35 | 36 | await fse.ensureDir("/tmp/manysketchs"); 37 | 38 | for (let i = 0; i < urllist.length; i++) { 39 | const { filename, url } = urllist[i]; 40 | 41 | const jsonFilePath = `/tmp/manysketchs/${filename}.json`; 42 | await downloadFile(url, jsonFilePath); 43 | 44 | // generate sketch file 45 | 46 | const json = await fse.readJSON(jsonFilePath); 47 | 48 | const newLayer = new Layer({ class: "SymbolMaster", data: json }); 49 | const newPage = new Page({ layers: [newLayer.toSketchJSON()] } as any); 50 | const newPack = new JSONPack({ pages: [newPage] } as any); 51 | 52 | const folderFilePath = `/tmp/manysketchs/${filename}`; 53 | await newPack.write(folderFilePath); 54 | await newPack.zip(`${folderFilePath}.sketch`); 55 | } 56 | })(); 57 | -------------------------------------------------------------------------------- /demo/new/new.ts: -------------------------------------------------------------------------------- 1 | import { JSONPack } from "../../src"; 2 | 3 | const pack = new JSONPack(); 4 | 5 | pack.writeSync("temp/new/new"); 6 | -------------------------------------------------------------------------------- /demo/new/with-image.ts: -------------------------------------------------------------------------------- 1 | import * as fse from "fs-extra"; 2 | import { get as _get, set as _set } from "lodash"; 3 | import { v4 } from "uuid"; 4 | import { JSONPack, Page, Layer } from "../../src"; 5 | 6 | const datajson = { 7 | imageList: [ 8 | "layers[2].layers[0].layers[1].layers[1].image", 9 | "layers[2].layers[2].layers[1].layers[1].image", 10 | "layers[2].layers[4].layers[1].layers[1].image", 11 | ], 12 | data: { 13 | _class: "symbolMaster", 14 | // ...SketchJSON 15 | isVisible: true, 16 | }, 17 | }; 18 | 19 | const json = datajson.data; 20 | const list = datajson.imageList; 21 | 22 | const folderFilePath = `/tmp/justtryone`; 23 | const sketchFilePath = `${folderFilePath}.sketch`; 24 | 25 | (async () => { 26 | const filelist: { name: string; data: string }[] = []; 27 | for (let i = 0; i < list.length; i++) { 28 | const whereWeHaveImage = list[i]; 29 | const imageJson = _get(json, whereWeHaveImage); 30 | 31 | const base64Image = imageJson.data?._data; 32 | const filename = `${v4()}.png`; 33 | 34 | // modify json 35 | 36 | const newImageJson = { 37 | _class: "MSJSONFileReference", 38 | _ref_class: "MSImageData", 39 | _ref: `images/${filename}`, 40 | }; 41 | 42 | _set(json, whereWeHaveImage, newImageJson); 43 | filelist.push({ name: filename, data: base64Image }); 44 | } 45 | 46 | // generate Sketch JSON pack 47 | 48 | const newLayer = new Layer({ class: "SymbolMaster", data: json }); 49 | const newPage = new Page({ layers: [newLayer.toSketchJSON()] } as any); 50 | const newPack = new JSONPack({ pages: [newPage] } as any); 51 | 52 | await newPack.write(folderFilePath); 53 | 54 | // after writing pack, insert images 55 | 56 | await fse.ensureDir(`${folderFilePath}/images/`); 57 | 58 | const tasklist = filelist.map((f) => { 59 | const { name, data } = f; 60 | return fse.writeFile(`${folderFilePath}/images/${name}`, data, { 61 | encoding: "base64", 62 | }); 63 | }); 64 | 65 | await Promise.all(tasklist); 66 | 67 | // zip the .sketch file 68 | 69 | await newPack.zip(sketchFilePath); 70 | })(); 71 | -------------------------------------------------------------------------------- /demo/split/split-from.ts: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | import { SketchType, SketchFile, JSONPack, Page, Layer } from "../../src"; 3 | 4 | const sampleSketchPath = "demo/files/1Symbol1Artboard.sketch"; 5 | const targetPath = "temp/split/split-from"; 6 | const packPath = path.join(targetPath, "pack"); 7 | const newPackPath = path.join(targetPath, "new"); 8 | 9 | (async () => { 10 | try { 11 | // get one of the layers from a Sketch file 12 | 13 | const sketch = new SketchFile(sampleSketchPath); 14 | await sketch.unzip(packPath); 15 | const pack = await JSONPack.fromPath(packPath); 16 | 17 | const pages = pack.getPages(); 18 | let somePage: Page; 19 | if (pages && pages.length) { 20 | somePage = pages[0]; 21 | } else { 22 | throw new Error("No page!"); 23 | } 24 | 25 | const layers = somePage.getLayers({ classes: ["Artboard"] }); 26 | let someLayer: SketchType.Layer; 27 | if (layers && layers.length) { 28 | someLayer = layers[0]; 29 | } else { 30 | throw new Error("No valid Layer!"); 31 | } 32 | 33 | // make a new Sketch file with that single layer 34 | 35 | const newLayer = new Layer({ class: "Artboard", data: someLayer }); 36 | const newPage = new Page({ layers: [newLayer.toSketchJSON()] } as any); 37 | const newPack = new JSONPack({ pages: [newPage] } as any); 38 | 39 | await newPack.write(newPackPath); 40 | await newPack.zip(path.join(path.dirname(newPackPath), "new.sketch")); 41 | } catch (error) { 42 | throw error; 43 | } 44 | })(); 45 | -------------------------------------------------------------------------------- /demo/symbol/symbol.ts: -------------------------------------------------------------------------------- 1 | import { SketchFile, JSONPack, Page } from "../../src"; 2 | 3 | const sampleSketchPath = "demo/files/SimpleButton.sketch"; 4 | 5 | const originSketch = new SketchFile(sampleSketchPath); 6 | 7 | const targetPath = "temp/symbol/files"; 8 | 9 | originSketch.unzip(targetPath).then(() => { 10 | let jsonPack; 11 | if (JSONPack.isValidStructure(targetPath)) { 12 | jsonPack = JSONPack.fromPathSync(targetPath); 13 | 14 | const pages: Page[] = jsonPack.getPages(); 15 | 16 | pages.forEach((page) => { 17 | page.symbolMasters().forEach((symbolMaster) => { 18 | console.log( 19 | `symbolMaster: ${symbolMaster.name}, ${symbolMaster.do_objectID}` 20 | ); 21 | }); 22 | }); 23 | } 24 | }); 25 | -------------------------------------------------------------------------------- /demo/symbol/symboljson.ts: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | import { JSONPack, Page, Layer } from "../../src"; 3 | 4 | const symbolJSONs = [ 5 | { 6 | _class: "symbolMaster", 7 | frame: { 8 | _class: "rect", 9 | constrainProportions: false, 10 | height: 21, 11 | width: 800, 12 | x: 0, 13 | y: 0, 14 | }, 15 | allowsOverrides: true, 16 | backgroundColor: { 17 | _class: "color", 18 | red: 1, 19 | green: 1, 20 | blue: 1, 21 | alpha: 1, 22 | }, 23 | booleanOperation: -1, 24 | changeIdentifier: 0, 25 | do_objectID: "9F0C4E24-6822-411C-BFB7-49703C9B8BAB", 26 | symbolID: "A56A226B-DB9D-4712-A932-6C48E7F3E9F1", 27 | exportOptions: { 28 | _class: "exportOptions", 29 | includedLayerIds: [], 30 | layerOptions: 0, 31 | shouldTrim: false, 32 | exportFormats: [], 33 | }, 34 | hasClickThrough: true, 35 | includeInCloudUpload: true, 36 | hasBackgroundColor: false, 37 | includeBackgroundColorInExport: true, 38 | resizesContent: false, 39 | includeBackgroundColorInInstance: false, 40 | nameIsFixed: false, 41 | horizontalRulerData: { _class: "rulerData", base: 0, guides: [] }, 42 | verticalRulerData: { _class: "rulerData", base: 0, guides: [] }, 43 | resizingConstraint: 1, 44 | resizingType: 1, 45 | groupLayout: { _class: "MSImmutableFreeformGroupLayout" }, 46 | isFixedToViewport: false, 47 | sharedStyleID: "", 48 | isFlippedHorizontal: false, 49 | isFlippedVertical: false, 50 | isLocked: false, 51 | isFlowHome: false, 52 | name: "", 53 | rotation: 0, 54 | layerListExpandedType: 0, 55 | overrideProperties: [ 56 | { 57 | _class: "MSImmutableOverrideProperty", 58 | canOverride: true, 59 | overrideName: "C3776604-4754-4C97-AB43-7BD0E81C99F2_stringValue", 60 | }, 61 | ], 62 | layers: [ 63 | { 64 | _class: "group", 65 | do_objectID: "32F2EEC5-22DA-41D1-B6E4-EAD25DB555E3", 66 | booleanOperation: -1, 67 | isFixedToViewport: false, 68 | isFlippedHorizontal: false, 69 | isFlippedVertical: false, 70 | isVisible: true, 71 | isLocked: false, 72 | layerListExpandedType: 0, 73 | name: "编组", 74 | nameIsFixed: false, 75 | resizingConstraint: 63, 76 | resizingType: 0, 77 | rotation: 0, 78 | shouldBreakMaskChain: false, 79 | exportOptions: { 80 | _class: "exportOptions", 81 | includedLayerIds: [], 82 | layerOptions: 0, 83 | shouldTrim: false, 84 | exportFormats: [], 85 | }, 86 | frame: { 87 | _class: "rect", 88 | constrainProportions: false, 89 | height: 21, 90 | width: 800, 91 | x: 0, 92 | y: 0, 93 | }, 94 | clippingMaskMode: 0, 95 | hasClippingMask: false, 96 | style: { 97 | _class: "style", 98 | do_objectID: "94DC1F0F-CE6D-4A02-A5AD-658CDDAF1095", 99 | endMarkerType: 0, 100 | miterLimit: 10, 101 | startMarkerType: 0, 102 | windingRule: 1, 103 | borderOptions: { 104 | _class: "borderOptions", 105 | isEnabled: true, 106 | dashPattern: [], 107 | lineCapStyle: 0, 108 | lineJoinStyle: 0, 109 | }, 110 | colorControls: { 111 | _class: "colorControls", 112 | isEnabled: false, 113 | brightness: 0, 114 | contrast: 1, 115 | hue: 0, 116 | saturation: 1, 117 | }, 118 | fills: [], 119 | borders: [], 120 | shadows: [], 121 | innerShadows: [], 122 | contextSettings: { 123 | _class: "graphicsContextSettings", 124 | blendMode: 0, 125 | opacity: 1, 126 | }, 127 | }, 128 | hasClickThrough: false, 129 | groupLayout: { _class: "MSImmutableFreeformGroupLayout" }, 130 | layers: [ 131 | { 132 | _class: "text", 133 | do_objectID: "C3776604-4754-4C97-AB43-7BD0E81C99F2", 134 | booleanOperation: -1, 135 | isFixedToViewport: false, 136 | isFlippedHorizontal: false, 137 | isFlippedVertical: false, 138 | isLocked: false, 139 | isVisible: true, 140 | name: "文本", 141 | nameIsFixed: false, 142 | layerListExpandedType: 0, 143 | resizingConstraint: 63, 144 | resizingType: 0, 145 | rotation: 0, 146 | shouldBreakMaskChain: false, 147 | exportOptions: { 148 | _class: "exportOptions", 149 | includedLayerIds: [], 150 | layerOptions: 0, 151 | shouldTrim: false, 152 | exportFormats: [], 153 | }, 154 | frame: { 155 | _class: "rect", 156 | constrainProportions: false, 157 | height: 21, 158 | width: 179.125, 159 | x: 0, 160 | y: 0, 161 | }, 162 | clippingMaskMode: 0, 163 | hasClippingMask: false, 164 | style: { 165 | _class: "style", 166 | do_objectID: "861858B9-6A61-4EFD-929A-910AE00BFD93", 167 | endMarkerType: 0, 168 | miterLimit: 10, 169 | startMarkerType: 0, 170 | windingRule: 1, 171 | borderOptions: { 172 | _class: "borderOptions", 173 | isEnabled: true, 174 | dashPattern: [], 175 | lineCapStyle: 0, 176 | lineJoinStyle: 0, 177 | }, 178 | colorControls: { 179 | _class: "colorControls", 180 | isEnabled: false, 181 | brightness: 0, 182 | contrast: 1, 183 | hue: 0, 184 | saturation: 1, 185 | }, 186 | fills: [], 187 | borders: [], 188 | shadows: [], 189 | innerShadows: [], 190 | contextSettings: { 191 | _class: "graphicsContextSettings", 192 | blendMode: 0, 193 | opacity: 1, 194 | }, 195 | }, 196 | attributedString: { 197 | _class: "attributedString", 198 | string: "这是示例一:带 HiTuSymbol", 199 | attributes: [ 200 | { 201 | _class: "stringAttribute", 202 | location: 0, 203 | length: 18, 204 | attributes: { 205 | underlineStyle: 0, 206 | MSAttributedStringTextTransformAttribute: 0, 207 | paragraphStyle: { 208 | _class: "paragraphStyle", 209 | maximumLineHeight: 21, 210 | minimumLineHeight: 21, 211 | }, 212 | kerning: 0, 213 | strikethroughStyle: 0, 214 | MSAttributedStringFontAttribute: { 215 | _class: "fontDescriptor", 216 | attributes: { name: "PingFangSC-Regular", size: 14 }, 217 | }, 218 | MSAttributedStringColorAttribute: { 219 | _class: "color", 220 | red: 0.5725490196078431, 221 | green: 0.4392156862745098, 222 | blue: 0.792156862745098, 223 | alpha: 1, 224 | }, 225 | }, 226 | }, 227 | ], 228 | }, 229 | automaticallyDrawOnUnderlyingPath: false, 230 | dontSynchroniseWithSymbol: false, 231 | lineSpacingBehaviour: 2, 232 | textBehaviour: 0, 233 | glyphBounds: "", 234 | }, 235 | ], 236 | userInfo: null, 237 | }, 238 | ], 239 | isVisible: true, 240 | }, 241 | { 242 | _class: "symbolMaster", 243 | frame: { 244 | _class: "rect", 245 | constrainProportions: false, 246 | height: 21, 247 | width: 800, 248 | x: 0, 249 | y: 21, 250 | }, 251 | allowsOverrides: true, 252 | backgroundColor: { _class: "color", red: 1, green: 1, blue: 1, alpha: 1 }, 253 | booleanOperation: -1, 254 | changeIdentifier: 0, 255 | do_objectID: "27E81847-351F-4E58-AB46-F0C3BBEFAA19", 256 | symbolID: "D52D4C7E-EAA1-4E07-8BA2-454BA780B928", 257 | exportOptions: { 258 | _class: "exportOptions", 259 | includedLayerIds: [], 260 | layerOptions: 0, 261 | shouldTrim: false, 262 | exportFormats: [], 263 | }, 264 | hasClickThrough: true, 265 | includeInCloudUpload: true, 266 | hasBackgroundColor: false, 267 | includeBackgroundColorInExport: true, 268 | resizesContent: false, 269 | includeBackgroundColorInInstance: false, 270 | nameIsFixed: false, 271 | horizontalRulerData: { _class: "rulerData", base: 0, guides: [] }, 272 | verticalRulerData: { _class: "rulerData", base: 0, guides: [] }, 273 | resizingConstraint: 1, 274 | resizingType: 1, 275 | groupLayout: { _class: "MSImmutableFreeformGroupLayout" }, 276 | isFixedToViewport: false, 277 | sharedStyleID: "", 278 | isFlippedHorizontal: false, 279 | isFlippedVertical: false, 280 | isLocked: false, 281 | isFlowHome: false, 282 | name: "", 283 | rotation: 0, 284 | layerListExpandedType: 0, 285 | overrideProperties: [ 286 | { 287 | _class: "MSImmutableOverrideProperty", 288 | canOverride: true, 289 | overrideName: "CBEBDCA6-8C65-41CE-A4AD-C238C18D960E_stringValue", 290 | }, 291 | ], 292 | layers: [ 293 | { 294 | _class: "group", 295 | do_objectID: "569F620A-02AC-498A-BFC0-EBE227D7D4C4", 296 | booleanOperation: -1, 297 | isFixedToViewport: false, 298 | isFlippedHorizontal: false, 299 | isFlippedVertical: false, 300 | isVisible: true, 301 | isLocked: false, 302 | layerListExpandedType: 0, 303 | name: "编组", 304 | nameIsFixed: false, 305 | resizingConstraint: 63, 306 | resizingType: 0, 307 | rotation: 0, 308 | shouldBreakMaskChain: false, 309 | exportOptions: { 310 | _class: "exportOptions", 311 | includedLayerIds: [], 312 | layerOptions: 0, 313 | shouldTrim: false, 314 | exportFormats: [], 315 | }, 316 | frame: { 317 | _class: "rect", 318 | constrainProportions: false, 319 | height: 21, 320 | width: 800, 321 | x: 0, 322 | y: 0, 323 | }, 324 | clippingMaskMode: 0, 325 | hasClippingMask: false, 326 | style: { 327 | _class: "style", 328 | do_objectID: "E7593B33-1954-4064-8EF6-EBD00CD80F70", 329 | endMarkerType: 0, 330 | miterLimit: 10, 331 | startMarkerType: 0, 332 | windingRule: 1, 333 | borderOptions: { 334 | _class: "borderOptions", 335 | isEnabled: true, 336 | dashPattern: [], 337 | lineCapStyle: 0, 338 | lineJoinStyle: 0, 339 | }, 340 | colorControls: { 341 | _class: "colorControls", 342 | isEnabled: false, 343 | brightness: 0, 344 | contrast: 1, 345 | hue: 0, 346 | saturation: 1, 347 | }, 348 | fills: [], 349 | borders: [], 350 | shadows: [], 351 | innerShadows: [], 352 | contextSettings: { 353 | _class: "graphicsContextSettings", 354 | blendMode: 0, 355 | opacity: 1, 356 | }, 357 | }, 358 | hasClickThrough: false, 359 | groupLayout: { _class: "MSImmutableFreeformGroupLayout" }, 360 | layers: [ 361 | { 362 | _class: "text", 363 | do_objectID: "CBEBDCA6-8C65-41CE-A4AD-C238C18D960E", 364 | booleanOperation: -1, 365 | isFixedToViewport: false, 366 | isFlippedHorizontal: false, 367 | isFlippedVertical: false, 368 | isLocked: false, 369 | isVisible: true, 370 | name: "文本", 371 | nameIsFixed: false, 372 | layerListExpandedType: 0, 373 | resizingConstraint: 63, 374 | resizingType: 0, 375 | rotation: 0, 376 | shouldBreakMaskChain: false, 377 | exportOptions: { 378 | _class: "exportOptions", 379 | includedLayerIds: [], 380 | layerOptions: 0, 381 | shouldTrim: false, 382 | exportFormats: [], 383 | }, 384 | frame: { 385 | _class: "rect", 386 | constrainProportions: false, 387 | height: 21, 388 | width: 179.125, 389 | x: 0, 390 | y: 0, 391 | }, 392 | clippingMaskMode: 0, 393 | hasClippingMask: false, 394 | style: { 395 | _class: "style", 396 | do_objectID: "9266C087-4CD8-4056-9961-0E4A60CE2C8A", 397 | endMarkerType: 0, 398 | miterLimit: 10, 399 | startMarkerType: 0, 400 | windingRule: 1, 401 | borderOptions: { 402 | _class: "borderOptions", 403 | isEnabled: true, 404 | dashPattern: [], 405 | lineCapStyle: 0, 406 | lineJoinStyle: 0, 407 | }, 408 | colorControls: { 409 | _class: "colorControls", 410 | isEnabled: false, 411 | brightness: 0, 412 | contrast: 1, 413 | hue: 0, 414 | saturation: 1, 415 | }, 416 | fills: [], 417 | borders: [], 418 | shadows: [], 419 | innerShadows: [], 420 | contextSettings: { 421 | _class: "graphicsContextSettings", 422 | blendMode: 0, 423 | opacity: 1, 424 | }, 425 | }, 426 | attributedString: { 427 | _class: "attributedString", 428 | string: "这是示例二:带 HiTuSymbol", 429 | attributes: [ 430 | { 431 | _class: "stringAttribute", 432 | location: 0, 433 | length: 18, 434 | attributes: { 435 | underlineStyle: 0, 436 | MSAttributedStringTextTransformAttribute: 0, 437 | paragraphStyle: { 438 | _class: "paragraphStyle", 439 | maximumLineHeight: 21, 440 | minimumLineHeight: 21, 441 | }, 442 | kerning: 0, 443 | strikethroughStyle: 0, 444 | MSAttributedStringFontAttribute: { 445 | _class: "fontDescriptor", 446 | attributes: { name: "PingFangSC-Regular", size: 14 }, 447 | }, 448 | MSAttributedStringColorAttribute: { 449 | _class: "color", 450 | red: 0.5725490196078431, 451 | green: 0.4392156862745098, 452 | blue: 0.792156862745098, 453 | alpha: 1, 454 | }, 455 | }, 456 | }, 457 | ], 458 | }, 459 | automaticallyDrawOnUnderlyingPath: false, 460 | dontSynchroniseWithSymbol: false, 461 | lineSpacingBehaviour: 2, 462 | textBehaviour: 0, 463 | glyphBounds: "", 464 | }, 465 | ], 466 | userInfo: null, 467 | }, 468 | ], 469 | isVisible: true, 470 | }, 471 | ]; 472 | 473 | const targetPath = "temp/symbol/symboljson"; 474 | const packPath = path.join(targetPath, "pack"); 475 | const filePath = path.join(targetPath, "file.sketch"); 476 | 477 | (async () => { 478 | try { 479 | const newLayers = symbolJSONs.map((json, index) => { 480 | const data = { ...json }; 481 | data.name = `asset-${index}`; 482 | 483 | return new Layer({ 484 | class: "SymbolMaster", 485 | data, 486 | }); 487 | }); 488 | 489 | const newPage = new Page({ 490 | name: "Assets", 491 | layers: newLayers.map((layer) => layer.toSketchJSON()), 492 | } as any); 493 | const newPack = new JSONPack({ pages: [newPage] } as any); 494 | 495 | await newPack.write(packPath); 496 | await newPack.zip(filePath); 497 | } catch (error) { 498 | throw error; 499 | } 500 | })(); 501 | -------------------------------------------------------------------------------- /demo/unzip/minisample.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ant-design/sketch-json-api/5d38a1fdaeaa1d0af94fb4ad390f327ca6b7dfe5/demo/unzip/minisample.sketch -------------------------------------------------------------------------------- /demo/unzip/unzip.ts: -------------------------------------------------------------------------------- 1 | import { SketchFile } from "../../src"; 2 | 3 | (async () => { 4 | const sampleSketchPath = "demo/unzip/minisample.sketch"; 5 | 6 | const sketch = new SketchFile(sampleSketchPath); 7 | await sketch.unzip("temp/unzip"); 8 | })(); 9 | -------------------------------------------------------------------------------- /demo/zip/rezip-with-bitmap.ts: -------------------------------------------------------------------------------- 1 | import { JSONPack, SketchFile } from "../../src"; 2 | 3 | const DEMO_SOURCE_FILE = "demo/files/bitmap.sketch"; 4 | const DEMO_PATH = "temp/zip/rezip-with-bitmap"; 5 | 6 | (async () => { 7 | const originSketch = new SketchFile(DEMO_SOURCE_FILE); 8 | const originFilePath = `${DEMO_PATH}/origin`; 9 | await originSketch.unzip(originFilePath); 10 | 11 | if (!JSONPack.isValidStructure(originFilePath)) return; 12 | 13 | const newPack = await JSONPack.fromPath(originFilePath); 14 | 15 | const infotest = await newPack.getAllArtboardsWithBitmapInfo(); 16 | console.log(infotest[0].userInfo?.bitmapInfos); 17 | 18 | await newPack.write(`${DEMO_PATH}/files`); 19 | await newPack.zip(`${DEMO_PATH}/result.sketch`); 20 | })(); 21 | -------------------------------------------------------------------------------- /demo/zip/zip.ts: -------------------------------------------------------------------------------- 1 | import { JSONPack } from "../../src"; 2 | 3 | const samplePackPath = "demo/files/miniSamplePack"; 4 | 5 | (async () => { 6 | const pack = await JSONPack.fromPath(samplePackPath); 7 | await pack.zip("temp/zip/generated.sketch"); 8 | })(); 9 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | collectCoverage: false, 3 | collectCoverageFrom: ["src/**/*.ts"], 4 | globals: { 5 | "ts-jest": { 6 | diagnostics: false, 7 | }, 8 | }, 9 | preset: "ts-jest", 10 | testEnvironment: "node", 11 | testPathIgnorePatterns: ["/node_modules/"], 12 | }; 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sketch-json-api", 3 | "version": "0.1.0-alpha.26", 4 | "description": "", 5 | "main": "dist/src/index.js", 6 | "scripts": { 7 | "demo-techui": "npm run build && rm -rf temp/techui && node dist/demo/label/techui/techui.js", 8 | "demo-zip": "npm run build && rm -rf temp/zip && node dist/demo/zip/zip.js", 9 | "demo-unzip": "npm run build && rm -rf temp/unzip &&node dist/demo/unzip/unzip.js", 10 | "sync-origin-types": "node dist/scripts/sync-origin-types.js", 11 | "build": "tsc", 12 | "test": "jest", 13 | "prepublish": "npm run build && npm run test" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/ant-design/sketch-json-api.git" 18 | }, 19 | "author": "", 20 | "license": "MIT", 21 | "bugs": { 22 | "url": "https://github.com/ant-design/sketch-json-api/issues" 23 | }, 24 | "homepage": "https://github.com/ant-design/sketch-json-api#readme", 25 | "dependencies": { 26 | "@sketch-hq/sketch-file-format": "^3.6.0", 27 | "@sketch-hq/sketch-file-format-ts": "5.2.2", 28 | "@types/json-schema": "^7.0.4", 29 | "@types/uuid": "^8.0.0", 30 | "compressing": "^1.5.1", 31 | "fs-extra": "^9.0.1", 32 | "mkdirp": "^1.0.4", 33 | "uuid": "^8.1.0" 34 | }, 35 | "devDependencies": { 36 | "@types/fs-extra": "^9.0.1", 37 | "@types/jest": "^26.0.0", 38 | "@types/lodash": "^4.14.161", 39 | "@types/node": "^14.0.12", 40 | "jest": "^26.0.1", 41 | "lodash": "^4.17.20", 42 | "ts-jest": "^26.1.0", 43 | "typescript": "^3.9.5", 44 | "urllib": "^2.36.1" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /scripts/sync-origin-types.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs-extra"; 2 | import * as path from "path"; 3 | 4 | (async () => { 5 | const file = path.join(process.cwd(), "src/types/origin.ts"); 6 | const data = await fs.readFile(file, { encoding: "utf8" }); 7 | 8 | const lines = data.split("\n"); 9 | 10 | let defLineIndex; 11 | let originTypesArrayStr; 12 | for (let i = 0; i < lines.length; i++) { 13 | if (lines[i] === "export const ORIGIN_TYPES = tuple(") { 14 | defLineIndex = i; 15 | originTypesArrayStr = "["; 16 | } 17 | 18 | if (defLineIndex && i > defLineIndex) { 19 | if (lines[i] === ");") { 20 | originTypesArrayStr += "]"; 21 | break; 22 | } else { 23 | originTypesArrayStr += lines[i]; 24 | } 25 | } 26 | } 27 | 28 | const originTypesArray = originTypesArrayStr 29 | ? JSON.parse(originTypesArrayStr).sort() 30 | : []; 31 | 32 | const fileContent = `// Formatted and synced by sync-origin-type 33 | import FileFormat from "@sketch-hq/sketch-file-format-ts"; 34 | import { tuple } from "./utils"; 35 | 36 | export const ORIGIN_TYPES = tuple( 37 | ${originTypesArray.map((e: string) => ` "${e}"`).join(",\n")} 38 | ); 39 | export type OriginTypes = typeof ORIGIN_TYPES[number]; 40 | 41 | ${originTypesArray 42 | .map((e: string) => `export type ${e} = FileFormat.${e};`) 43 | .join("\n")} 44 | `; 45 | 46 | console.log(fileContent); 47 | 48 | fs.writeFile(file, fileContent); 49 | })(); 50 | -------------------------------------------------------------------------------- /src/constants/index.ts: -------------------------------------------------------------------------------- 1 | export const CONSTANTS = { 2 | user: { 3 | document: { 4 | pageListCollapsed: { 5 | default: 85, 6 | }, 7 | pageListHeight: { 8 | default: 0, 9 | }, 10 | }, 11 | }, 12 | }; 13 | 14 | export { INIT_DATA } from "./init"; 15 | -------------------------------------------------------------------------------- /src/constants/init.ts: -------------------------------------------------------------------------------- 1 | import SketchType from "../types"; 2 | 3 | type InitData = Partial< 4 | Record 5 | >; 6 | 7 | const Color = { 8 | _class: "color", 9 | alpha: 1, 10 | blue: 0, 11 | green: 0, 12 | red: 0, 13 | }; 14 | 15 | const ParagraphStyle = { 16 | _class: "paragraphStyle", 17 | alignment: 0, 18 | }; 19 | 20 | const TextStyle = { 21 | _class: "textStyle", 22 | encodedAttributes: { 23 | MSAttributedStringFontAttribute: { 24 | _class: "fontDescriptor", 25 | attributes: { 26 | name: "Helvetica", 27 | size: 36, 28 | }, 29 | }, 30 | MSAttributedStringColorAttribute: Color, 31 | textStyleVerticalAlignmentKey: 0, 32 | underlineStyle: 0, 33 | strikethroughStyle: 0, 34 | paragraphStyle: ParagraphStyle, 35 | }, 36 | verticalAlignment: 0, 37 | kerning: 0, 38 | }; 39 | 40 | const GraphicsContextSettings = { 41 | _class: "graphicsContextSettings", 42 | blendMode: 0, 43 | opacity: 1, 44 | }; 45 | 46 | const BorderOptions = { 47 | _class: "borderOptions", 48 | isEnabled: true, 49 | dashPattern: [], 50 | lineCapStyle: 1, 51 | lineJoinStyle: 1, 52 | }; 53 | 54 | const ColorControls = { 55 | _class: "colorControls", 56 | isEnabled: false, 57 | brightness: 0, 58 | contrast: 1, 59 | hue: 0, 60 | saturation: 1, 61 | }; 62 | 63 | const ExportOptions = { 64 | _class: "exportOptions", 65 | includedLayerIds: [], 66 | layerOptions: 0, 67 | shouldTrim: false, 68 | exportFormats: [], 69 | }; 70 | 71 | const RulerData = { _class: "rulerData", base: 0, guides: [] }; 72 | 73 | export const INIT_DATA: InitData = { 74 | Color, 75 | ParagraphStyle, 76 | TextStyle, 77 | GraphicsContextSettings, 78 | BorderOptions, 79 | ColorControls, 80 | ExportOptions, 81 | RulerData, 82 | }; 83 | 84 | export default INIT_DATA; 85 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * as SketchType from "./types"; 2 | 3 | export { labelSymbolMaster, markLayer } from "./utils"; 4 | 5 | export { 6 | JSONPack, 7 | JSONPackConstructorOptions, 8 | ZipOpts, 9 | } from "./structures/JSONPack"; 10 | export { SketchFile, UnzipOpts } from "./structures/SketchFile"; 11 | 12 | export { Meta } from "./structures/Meta"; 13 | export { User } from "./structures/User"; 14 | export { Document } from "./structures/Document"; 15 | export { Page } from "./structures/Page"; 16 | 17 | export { Layer, LayerConstrOpts } from "./structures/Layer"; 18 | -------------------------------------------------------------------------------- /src/structures/Document.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | import { v4 } from "uuid"; 3 | 4 | import SketchType from "../types"; 5 | import { Page } from "./Page"; 6 | 7 | export class Document { 8 | static _class: "document" = "document"; 9 | 10 | do_objectID: SketchType.Uuid; 11 | pages: SketchType.FileRef[]; 12 | 13 | assets: SketchType.AssetCollection; 14 | colorSpace: SketchType.ColorSpace; 15 | currentPageIndex: number; 16 | foreignLayerStyles: SketchType.ForeignLayerStyle[]; 17 | foreignSymbols: SketchType.ForeignSymbol[]; 18 | foreignTextStyles: SketchType.ForeignTextStyle[]; 19 | layerStyles: SketchType.SharedStyleContainer; 20 | layerTextStyles: SketchType.SharedTextStyleContainer; 21 | 22 | constructor(); 23 | constructor(options: SketchType.Document); 24 | constructor(options?: SketchType.Document, pages?: Page[]); 25 | constructor(options?: any, pages?: any) { 26 | this.do_objectID = (options && options.do_objectID) || v4().toUpperCase(); 27 | 28 | if (pages) { 29 | this.pages = pages.map((page: Page) => { 30 | const pageRef: SketchType.FileRef = { 31 | _class: "MSJSONFileReference", 32 | _ref_class: "MSImmutablePage", 33 | _ref: `pages/${page.getPageId()}`, 34 | }; 35 | return pageRef; 36 | }); 37 | } else if (options && options.pages && options.pages.length) { 38 | this.pages = options.pages; 39 | } else { 40 | const atLeastOnePage = new Page(); 41 | const pageRef: SketchType.FileRef = { 42 | _class: "MSJSONFileReference", 43 | _ref_class: "MSImmutablePage", 44 | _ref: `pages/${atLeastOnePage.getPageId()}`, 45 | }; 46 | this.pages = [pageRef]; 47 | } 48 | 49 | this.assets = (options && options.assets) || { 50 | _class: "assetCollection", 51 | colors: [], 52 | gradients: [], 53 | imageCollection: { 54 | _class: "imageCollection", 55 | images: {}, 56 | }, 57 | images: [], 58 | }; 59 | 60 | this.colorSpace = (options && options.colorSpace) || 0; 61 | this.currentPageIndex = (options && options.currentPageIndex) || 1; 62 | this.foreignLayerStyles = (options && options.foreignLayerStyles) || []; 63 | this.foreignSymbols = (options && options.foreignSymbols) || []; 64 | this.foreignTextStyles = (options && options.foreignTextStyles) || []; 65 | this.layerStyles = (options && options.layerStyles) || { 66 | _class: "sharedStyleContainer", 67 | objects: [], 68 | }; 69 | this.layerTextStyles = (options && options.layerTextStyles) || { 70 | _class: "sharedTextStyleContainer", 71 | objects: [], 72 | }; 73 | } 74 | 75 | updateProps(options?: SketchType.Document): void; 76 | updateProps(options?: any) { 77 | Object.keys(options).forEach((prop) => { 78 | if (this.hasOwnProperty(prop)) { 79 | this[prop as keyof this] = options[prop]; 80 | } 81 | }); 82 | } 83 | 84 | static fromData(options: SketchType.Document): Document { 85 | const document = new this(); 86 | document.updateProps(options); 87 | return document; 88 | } 89 | 90 | static fromPath(path: string): Document { 91 | const file = fs.readFileSync(path, "utf-8"); 92 | if (file) { 93 | const document = new this(); 94 | document.updateProps(JSON.parse(file)); 95 | return document; 96 | } else { 97 | throw Error("Invalid data from path."); 98 | } 99 | } 100 | 101 | toSketchJSON(): SketchType.Document { 102 | return { 103 | _class: Document._class, 104 | 105 | do_objectID: this.do_objectID, 106 | pages: this.pages, 107 | 108 | assets: this.assets, 109 | colorSpace: this.colorSpace, 110 | currentPageIndex: this.currentPageIndex, 111 | foreignLayerStyles: this.foreignLayerStyles, 112 | foreignSymbols: this.foreignSymbols, 113 | foreignTextStyles: this.foreignTextStyles, 114 | layerStyles: this.layerStyles, 115 | layerTextStyles: this.layerTextStyles, 116 | }; 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/structures/JSONPack.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | import * as fse from "fs-extra"; 3 | import * as fsc from "../utils/fs-custom"; 4 | import * as path from "path"; 5 | import { exec } from "child_process"; 6 | import { promisify } from "util"; 7 | import { zip } from "compressing"; 8 | import { pipeline } from "stream"; 9 | 10 | import { User } from "./User"; 11 | import { Meta } from "./Meta"; 12 | import { Document } from "./Document"; 13 | import { Page } from "./Page"; 14 | import SketchType, { JSONPackComponent } from "../types"; 15 | 16 | import { bitmap2base64Sync, findAllBitmapInSketchJSON } from "../utils/image"; 17 | 18 | const pipe = promisify(pipeline); 19 | 20 | const STRUCTURE: Record< 21 | JSONPackComponent, 22 | { fileOrDir: "file" | "dir"; path: string; required: boolean } 23 | > = { 24 | user: { 25 | fileOrDir: "file", 26 | path: "user.json", 27 | required: true, 28 | }, 29 | meta: { 30 | fileOrDir: "file", 31 | path: "meta.json", 32 | required: true, 33 | }, 34 | document: { 35 | fileOrDir: "file", 36 | path: "document.json", 37 | required: true, 38 | }, 39 | pages: { 40 | fileOrDir: "dir", 41 | path: "pages", 42 | required: true, 43 | }, 44 | images: { 45 | fileOrDir: "dir", 46 | path: "images", 47 | required: false, 48 | }, 49 | }; 50 | 51 | export type Image = { 52 | fileName: string; 53 | path?: string; 54 | base64: string; 55 | }; 56 | 57 | export type JSONPackConstructorOptions = { 58 | user: User; 59 | meta: Meta; 60 | document: Document; 61 | pages: Page[]; 62 | images?: Image[]; 63 | path?: string; 64 | }; 65 | 66 | export interface ZipOpts { 67 | cli?: boolean; 68 | } 69 | 70 | export class JSONPack { 71 | user: User; 72 | meta: Meta; 73 | document: Document; 74 | pages: Page[]; 75 | images?: Image[]; 76 | path?: string; 77 | 78 | constructor(); 79 | constructor(options: JSONPackConstructorOptions); 80 | constructor(options?: any) { 81 | this.path = (options && options.path) || null; 82 | this.user = (options && options.user) || new User(); 83 | 84 | // at least one page 85 | let atLeastOnePage; 86 | if (options && options.pages && options.pages.length) { 87 | this.pages = options.pages; 88 | } else { 89 | atLeastOnePage = new Page(); 90 | this.pages = [atLeastOnePage]; 91 | } 92 | 93 | if (options && options.meta) { 94 | this.meta = options.meta; 95 | } else { 96 | this.meta = new Meta(undefined, this.pages); 97 | } 98 | 99 | if (options && options.document) { 100 | this.document = options.document; 101 | } else { 102 | this.document = new Document(undefined, this.pages); 103 | } 104 | 105 | if (options?.images) { 106 | this.images = options.images; 107 | } 108 | } 109 | 110 | static fromPathSync(packPath: string): JSONPack { 111 | if (!JSONPack.isValidStructure(packPath)) { 112 | throw Error("Invalid structure of path."); 113 | } 114 | 115 | const user = User.fromPath(path.join(packPath, "user.json")); 116 | const meta = Meta.fromPath(path.join(packPath, "meta.json")); 117 | const document = Document.fromPath(path.join(packPath, "document.json")); 118 | const pages = fs 119 | .readdirSync(path.join(packPath, "pages")) 120 | .map((pagePath) => Page.fromPath(path.join(packPath, "pages", pagePath))); 121 | 122 | const args: JSONPackConstructorOptions = { 123 | user, 124 | meta, 125 | document, 126 | pages, 127 | path: packPath, 128 | }; 129 | 130 | // maybe images 131 | if (fs.existsSync(path.join(packPath, "images"))) { 132 | const imageFileNames = fs.readdirSync(path.join(packPath, "images")); 133 | if (imageFileNames?.length) { 134 | const base64Images: Image[] = imageFileNames.map((fileName) => { 135 | const pat = path.join(packPath, "images", fileName); 136 | const base64Str = bitmap2base64Sync(pat); 137 | return { 138 | fileName, 139 | base64: base64Str, 140 | }; 141 | }); 142 | 143 | args.images = base64Images; 144 | } 145 | } 146 | 147 | return new this(args); 148 | } 149 | 150 | static fromPath(packPath: string): Promise { 151 | return new Promise((resolve, reject) => { 152 | if (!JSONPack.isValidStructure(packPath)) { 153 | reject("Invalid structure of path."); 154 | } 155 | 156 | const user = User.fromPath(path.join(packPath, "user.json")); 157 | const meta = Meta.fromPath(path.join(packPath, "meta.json")); 158 | const document = Document.fromPath(path.join(packPath, "document.json")); 159 | const pages = fs 160 | .readdirSync(path.join(packPath, "pages")) 161 | .map((pagePath) => 162 | Page.fromPath(path.join(packPath, "pages", pagePath)) 163 | ); 164 | 165 | const args: JSONPackConstructorOptions = { 166 | user, 167 | meta, 168 | document, 169 | pages, 170 | path: packPath, 171 | }; 172 | 173 | // maybe images 174 | if (fs.existsSync(path.join(packPath, "images"))) { 175 | const imageFileNames = fs.readdirSync(path.join(packPath, "images")); 176 | if (imageFileNames?.length) { 177 | const base64Images: Image[] = imageFileNames.map((fileName) => { 178 | const pat = path.join(packPath, "images", fileName); 179 | const base64Str = bitmap2base64Sync(pat); 180 | return { 181 | fileName, 182 | base64: base64Str, 183 | }; 184 | }); 185 | 186 | args.images = base64Images; 187 | } 188 | } 189 | 190 | const pack = new this(args); 191 | 192 | resolve(pack); 193 | }); 194 | } 195 | 196 | getPages() { 197 | return this.pages; 198 | } 199 | 200 | getImages() { 201 | return this.images; 202 | } 203 | 204 | setPath(path: string) { 205 | this.path = path; 206 | } 207 | 208 | async write(packPath: string): Promise { 209 | this.path = packPath; 210 | await fsc.resetPath(this.path); 211 | 212 | const userPromise = fsc.writeFile( 213 | path.join(packPath, "user.json"), 214 | JSON.stringify(this.user.toSketchJSON()) 215 | ); 216 | const metaPromise = fsc.writeFile( 217 | path.join(packPath, "meta.json"), 218 | JSON.stringify(this.meta.toSketchJSON()) 219 | ); 220 | const documentPromise = fsc.writeFile( 221 | path.join(packPath, "document.json"), 222 | JSON.stringify(this.document.toSketchJSON()) 223 | ); 224 | 225 | const pagePromises = this.pages.map((page) => { 226 | return fsc.writeFile( 227 | path.join(packPath, `pages/${page.getPageId()}.json`), 228 | JSON.stringify(page.toSketchJSON()) 229 | ); 230 | }); 231 | 232 | const allPromises = [ 233 | userPromise, 234 | metaPromise, 235 | documentPromise, 236 | ...pagePromises, 237 | ]; 238 | 239 | if (this.images) { 240 | const imagesPromises = this.images.map((image) => { 241 | image.path = path.join(packPath, "images", image.fileName); 242 | return fsc.writeFile(image.path, image.base64, { encoding: "base64" }); 243 | }); 244 | 245 | allPromises.push(...imagesPromises); 246 | } 247 | 248 | return new Promise((resolve, reject) => { 249 | Promise.all(allPromises) 250 | .then(() => { 251 | resolve(); 252 | }) 253 | .catch((err) => reject(err)); 254 | }); 255 | } 256 | 257 | writeSync(packPath: string) { 258 | this.path = packPath; 259 | fsc.resetPathSync(this.path); 260 | 261 | fsc.writeFileSync( 262 | path.join(packPath, "user.json"), 263 | JSON.stringify(this.user.toSketchJSON()) 264 | ); 265 | fsc.writeFileSync( 266 | path.join(packPath, "meta.json"), 267 | JSON.stringify(this.meta.toSketchJSON()) 268 | ); 269 | fsc.writeFileSync( 270 | path.join(packPath, "document.json"), 271 | JSON.stringify(this.document.toSketchJSON()) 272 | ); 273 | 274 | this.pages.forEach((page) => { 275 | fsc.writeFileSync( 276 | path.join(packPath, `pages/${page.getPageId()}.json`), 277 | JSON.stringify(page.toSketchJSON()) 278 | ); 279 | }); 280 | 281 | if (this.images) { 282 | this.images.forEach((image) => { 283 | image.path = path.join(packPath, "images", image.fileName); 284 | fsc.writeFileSync(image.path, image.base64, { encoding: "base64" }); 285 | }); 286 | } 287 | } 288 | 289 | static isValidStructure(packPath: string): boolean { 290 | if (packPath && fs.existsSync(packPath)) { 291 | const keys = Object.keys(STRUCTURE) as JSONPackComponent[]; 292 | keys.forEach((key) => { 293 | const value = STRUCTURE[key]; 294 | const componentPath = path.join(packPath, value.path); 295 | if (value.required) { 296 | if (value.fileOrDir === "file" && !fs.existsSync(componentPath)) { 297 | return false; 298 | } 299 | if (value.fileOrDir === "dir") { 300 | if ( 301 | !fs.existsSync(componentPath) || 302 | !fs.readdirSync(componentPath)?.length 303 | ) { 304 | return false; 305 | } 306 | } 307 | } 308 | }); 309 | 310 | return true; 311 | } 312 | 313 | return false; 314 | } 315 | 316 | async zip(sketchPath: string, options?: ZipOpts): Promise { 317 | if (!this.path) { 318 | throw Error( 319 | "Please firstly write() once or set the path for this JSON pack." 320 | ); 321 | } 322 | 323 | if (!JSONPack.isValidStructure(this.path)) { 324 | await this.write(this.path); 325 | } 326 | 327 | // check again 328 | if (!JSONPack.isValidStructure(this.path)) { 329 | throw Error(`The structure of this JSON pack is invalid! ${sketchPath}`); 330 | } 331 | 332 | fse.ensureDir(path.dirname(sketchPath)); 333 | 334 | if (options?.cli) { 335 | await promisify(exec)( 336 | `zip -r -X ${path.resolve(process.cwd(), sketchPath)} *`, 337 | { cwd: this.path } 338 | ); 339 | } else { 340 | const zipStream = new zip.Stream(); 341 | const files = await fse.readdir(this.path); 342 | for (let i = 0; i < files.length; i++) { 343 | const file = files[i]; 344 | zipStream.addEntry(path.join(this.path, file)); 345 | } 346 | const destStream = fse.createWriteStream(sketchPath); 347 | await pipe(zipStream, destStream); 348 | } 349 | } 350 | 351 | getAllArtboards(): SketchType.ArtboardLike[] { 352 | const allArtboards: SketchType.ArtboardLike[] = []; 353 | this.pages.forEach((page) => { 354 | page.artboards().forEach((artboard) => { 355 | allArtboards.push(artboard); 356 | }); 357 | }); 358 | return allArtboards; 359 | } 360 | 361 | async getAllArtboardsWithBitmapInfo(): Promise { 362 | if (!this.path) { 363 | throw Error( 364 | "Please firstly write() once or set the path for this JSON pack." 365 | ); 366 | } 367 | 368 | const allArtboards: SketchType.ArtboardLike[] = []; 369 | 370 | for (let i = 0; i < this.pages.length; i++) { 371 | const page = this.pages[i]; 372 | 373 | const artboards = page.artboards(); 374 | 375 | for (let j = 0; j < artboards.length; j++) { 376 | const artboard = artboards[j]; 377 | 378 | const infos = await findAllBitmapInSketchJSON( 379 | artboard, 380 | this.path as string 381 | ); 382 | if (!artboard.userInfo) { 383 | artboard.userInfo = {}; 384 | } 385 | artboard.userInfo["bitmapInfos"] = infos; 386 | 387 | allArtboards.push(artboard); 388 | } 389 | } 390 | return allArtboards; 391 | } 392 | } 393 | -------------------------------------------------------------------------------- /src/structures/Layer.ts: -------------------------------------------------------------------------------- 1 | import { v4 } from "uuid"; 2 | 3 | import SketchType from "../types"; 4 | import { assignDeep } from "../utils"; 5 | 6 | export interface LayerConstrOpts { 7 | class?: SketchType.LayerClass; 8 | data?: any; // TODO: SketchType.Layer 9 | } 10 | 11 | const randomSymbolMaster = () => { 12 | return { 13 | _class: "symbolMaster", 14 | frame: { 15 | _class: "rect", 16 | constrainProportions: false, 17 | height: 200, 18 | width: 200, 19 | x: 0, 20 | y: 0, 21 | }, 22 | allowsOverrides: true, 23 | backgroundColor: { 24 | _class: "color", 25 | red: 1, 26 | green: 1, 27 | blue: 1, 28 | alpha: 1, 29 | }, 30 | booleanOperation: -1, 31 | changeIdentifier: 0, 32 | do_objectID: v4().toUpperCase(), 33 | symbolID: v4().toUpperCase(), 34 | exportOptions: { 35 | _class: "exportOptions", 36 | includedLayerIds: [], 37 | layerOptions: 0, 38 | shouldTrim: false, 39 | exportFormats: [], 40 | }, 41 | hasClickThrough: true, 42 | includeInCloudUpload: true, 43 | hasBackgroundColor: false, 44 | includeBackgroundColorInExport: true, 45 | resizesContent: false, 46 | includeBackgroundColorInInstance: false, 47 | nameIsFixed: false, 48 | horizontalRulerData: { _class: "rulerData", base: 0, guides: [] }, 49 | verticalRulerData: { _class: "rulerData", base: 0, guides: [] }, 50 | resizingConstraint: 1, 51 | resizingType: 1, 52 | isFixedToViewport: false, 53 | sharedStyleID: "", 54 | isFlippedHorizontal: false, 55 | isFlippedVertical: false, 56 | isLocked: false, 57 | isFlowHome: false, 58 | name: "SymbolMaster", 59 | rotation: 0, 60 | layerListExpandedType: 0, 61 | overrideProperties: [], 62 | layers: [], 63 | isVisible: true, 64 | }; 65 | }; 66 | 67 | const randomArtboard = () => { 68 | return { 69 | _class: "artboard", 70 | do_objectID: v4().toUpperCase(), 71 | booleanOperation: -1, 72 | isFixedToViewport: false, 73 | isFlippedHorizontal: false, 74 | isFlippedVertical: false, 75 | isLocked: false, 76 | isVisible: true, 77 | layerListExpandedType: 0, 78 | name: "Artboard", 79 | nameIsFixed: false, 80 | resizingConstraint: 63, 81 | resizingType: 0, 82 | rotation: 0, 83 | shouldBreakMaskChain: true, 84 | exportOptions: { 85 | _class: "exportOptions", 86 | includedLayerIds: [], 87 | layerOptions: 0, 88 | shouldTrim: false, 89 | exportFormats: [], 90 | }, 91 | frame: { 92 | _class: "rect", 93 | constrainProportions: false, 94 | height: 200, 95 | width: 300, 96 | x: 100, 97 | y: 100, 98 | }, 99 | clippingMaskMode: 0, 100 | hasClippingMask: false, 101 | style: { 102 | _class: "style", 103 | do_objectID: v4().toUpperCase(), 104 | endMarkerType: 0, 105 | miterLimit: 10, 106 | startMarkerType: 0, 107 | windingRule: 1, 108 | blur: { 109 | _class: "blur", 110 | isEnabled: false, 111 | center: "{0.5, 0.5}", 112 | motionAngle: 0, 113 | radius: 10, 114 | saturation: 1, 115 | type: 0, 116 | }, 117 | borderOptions: { 118 | _class: "borderOptions", 119 | isEnabled: true, 120 | dashPattern: [], 121 | lineCapStyle: 0, 122 | lineJoinStyle: 0, 123 | }, 124 | borders: [], 125 | colorControls: { 126 | _class: "colorControls", 127 | isEnabled: false, 128 | brightness: 0, 129 | contrast: 1, 130 | hue: 0, 131 | saturation: 1, 132 | }, 133 | contextSettings: { 134 | _class: "graphicsContextSettings", 135 | blendMode: 0, 136 | opacity: 1, 137 | }, 138 | fills: [], 139 | innerShadows: [], 140 | shadows: [], 141 | }, 142 | hasClickThrough: false, 143 | groupLayout: { _class: "MSImmutableFreeformGroupLayout" }, 144 | layers: [], 145 | hasBackgroundColor: false, 146 | includeBackgroundColorInExport: true, 147 | includeInCloudUpload: true, 148 | isFlowHome: false, 149 | presetDictionary: {}, 150 | resizesContent: false, 151 | backgroundColor: { 152 | _class: "color", 153 | alpha: 1, 154 | blue: 1, 155 | green: 1, 156 | red: 1, 157 | }, 158 | horizontalRulerData: { _class: "rulerData", base: 0, guides: [] }, 159 | verticalRulerData: { _class: "rulerData", base: 0, guides: [] }, 160 | }; 161 | }; 162 | 163 | export class Layer { 164 | class: SketchType.LayerClass; 165 | data: any; // TODO: SketchType.Layer 166 | 167 | constructor(); 168 | constructor(options: LayerConstrOpts); 169 | constructor(options?: any) { 170 | const _class = options?.class || "Artboard"; 171 | let _data = options?.data; 172 | if (!_data) { 173 | if (options?.class === "SymbolMaster") { 174 | _data = randomSymbolMaster(); 175 | } else { 176 | _data = randomArtboard(); 177 | } 178 | } 179 | 180 | if (!_data._class || _class.toLowerCase() !== _data._class.toLowerCase()) { 181 | throw new Error( 182 | `Class (${options.class}) and data (${options.data._class}) can not match!` 183 | ); 184 | } 185 | 186 | this.class = _class; 187 | this.data = _data; 188 | } 189 | 190 | updateProps(options?: LayerConstrOpts): void; 191 | updateProps(options?: any) { 192 | Object.keys(options).forEach((prop) => { 193 | if (this.hasOwnProperty(prop)) { 194 | this[prop as keyof this] = assignDeep( 195 | {}, 196 | this[prop as keyof this], 197 | options[prop] 198 | ); 199 | } 200 | }); 201 | } 202 | 203 | toSketchJSON(): SketchType.Layer { 204 | return JSON.parse(JSON.stringify(this.data)); 205 | } 206 | 207 | static symbolToArtboard( 208 | symbol: SketchType.SymbolMaster 209 | ): SketchType.Artboard { 210 | const outerArtboard = new Layer({ class: "Artboard", data: null }); 211 | outerArtboard.updateProps({ 212 | data: { 213 | name: symbol.name, 214 | frame: symbol.frame, 215 | layers: symbol.layers, 216 | } as SketchType.Artboard, 217 | }); 218 | return outerArtboard.toSketchJSON() as SketchType.Artboard; 219 | } 220 | 221 | static artboardToSymbol( 222 | artboard: SketchType.Artboard 223 | ): SketchType.SymbolMaster { 224 | const outerSymbol = new Layer({ 225 | class: "SymbolMaster", 226 | data: null, 227 | }); 228 | outerSymbol.updateProps({ 229 | data: { 230 | name: artboard.name, 231 | frame: artboard.frame, 232 | layers: artboard.layers, 233 | }, 234 | }); 235 | 236 | return outerSymbol.toSketchJSON() as SketchType.SymbolMaster; 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /src/structures/Meta.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | 3 | import SketchType from "../types"; 4 | import { makeid } from "../utils"; 5 | import { Page } from "./Page"; 6 | import { PagesAndArtboards } from "./models/PagesAndArtboards"; 7 | 8 | export class Meta { 9 | commit: string; 10 | pagesAndArtboards: SketchType.PagesAndArtboards; 11 | version: SketchType.Version; 12 | fonts: string[]; 13 | compatibilityVersion: 99; 14 | app: SketchType.BundleId; 15 | autosaved: SketchType.NumericalBool; 16 | variant: SketchType.SketchVariant; 17 | created: { 18 | commit: string; 19 | appVersion: string; 20 | build: number; 21 | app: SketchType.BundleId; 22 | compatibilityVersion: number; 23 | version: number; 24 | variant: SketchType.SketchVariant; 25 | }; 26 | saveHistory: string[]; 27 | appVersion: string; 28 | build: number; 29 | 30 | constructor(); 31 | constructor(options: SketchType.Meta); 32 | constructor(options?: SketchType.Meta, pages?: Page[]); 33 | constructor(options?: any, pages?: any) { 34 | this.commit = (options && options.commit) || makeid(40); 35 | 36 | if (pages) { 37 | this.pagesAndArtboards = PagesAndArtboards.fromPages( 38 | pages 39 | ).toSketchJSON(); 40 | } else if ( 41 | options && 42 | options.pagesAndArtboards && 43 | Object.keys(options.pagesAndArtboards).length 44 | ) { 45 | this.pagesAndArtboards = options.pagesAndArtboards; 46 | } else { 47 | const atLeastOnePage = new Page(); 48 | const initPagesAndArtboards: SketchType.PagesAndArtboards = {}; 49 | initPagesAndArtboards[atLeastOnePage.getPageId()] = { 50 | name: atLeastOnePage.toSketchJSON().name, 51 | artboards: {}, 52 | }; 53 | this.pagesAndArtboards = initPagesAndArtboards; 54 | } 55 | 56 | this.version = (options && options.version) || 130; 57 | this.fonts = (options && options.fonts) || []; 58 | this.compatibilityVersion = (options && options.compatibilityVersion) || 99; 59 | this.app = 60 | (options && options.app) || 61 | SketchType.FileFormat.default.BundleId.PublicRelease; 62 | this.autosaved = (options && options.autosaved) || 0; 63 | this.variant = (options && options.variant) || "NONAPPSTORE"; 64 | this.created = (options && options.created) || { 65 | commit: this.commit, 66 | appVersion: "66.1", 67 | build: 97080, 68 | app: SketchType.FileFormat.default.BundleId.PublicRelease, 69 | compatibilityVersion: 99, 70 | version: 130, 71 | variant: "NONAPPSTORE", 72 | }; 73 | this.saveHistory = (options && options.saveHistory) || [ 74 | "NONAPPSTORE.97080", 75 | ]; 76 | this.appVersion = (options && options.appVersion) || "66.1"; 77 | this.build = (options && options.build) || 97080; 78 | } 79 | 80 | updateProps(options?: SketchType.Meta): void; 81 | updateProps(options?: any) { 82 | Object.keys(options).forEach((prop) => { 83 | if (this.hasOwnProperty(prop)) { 84 | this[prop as keyof this] = options[prop]; 85 | } 86 | }); 87 | } 88 | 89 | static fromData(options: SketchType.Meta): Meta { 90 | const meta = new this(); 91 | meta.updateProps(options); 92 | return meta; 93 | } 94 | 95 | static fromPath(path: string): Meta { 96 | const file = fs.readFileSync(path, "utf-8"); 97 | if (file) { 98 | const meta = new this(); 99 | meta.updateProps(JSON.parse(file)); 100 | return meta; 101 | } else { 102 | throw Error("Invalid data from path."); 103 | } 104 | } 105 | 106 | toSketchJSON(): SketchType.Meta { 107 | return { 108 | commit: this.commit, 109 | pagesAndArtboards: this.pagesAndArtboards, 110 | version: this.version, 111 | fonts: this.fonts, 112 | compatibilityVersion: this.compatibilityVersion, 113 | app: this.app, 114 | autosaved: this.autosaved, 115 | variant: this.variant, 116 | created: this.created, 117 | saveHistory: this.saveHistory, 118 | appVersion: this.appVersion, 119 | build: this.build, 120 | }; 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/structures/Page.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | import { v4 } from "uuid"; 3 | 4 | import SketchType from "../types"; 5 | import { Rect } from "./models"; 6 | import { INIT_DATA } from "../constants"; 7 | 8 | export class Page { 9 | static _class: "page" = "page"; 10 | 11 | do_objectID: SketchType.Uuid; 12 | booleanOperation: SketchType.BooleanOperation; 13 | exportOptions: SketchType.ExportOptions; 14 | frame: SketchType.Rect; 15 | isFixedToViewport: boolean; 16 | isFlippedHorizontal: boolean; 17 | isFlippedVertical: boolean; 18 | isLocked: boolean; 19 | isVisible: boolean; 20 | layerListExpandedType: SketchType.LayerListExpanded; 21 | name: string; 22 | nameIsFixed: boolean; 23 | resizingConstraint: number; 24 | resizingType: SketchType.ResizeType; 25 | rotation: number; 26 | shouldBreakMaskChain: boolean; 27 | hasClickThrough: boolean; 28 | layers: SketchType.Layer[]; 29 | includeInCloudUpload: boolean; 30 | horizontalRulerData: SketchType.RulerData; 31 | verticalRulerData: SketchType.RulerData; 32 | 33 | constructor(); 34 | constructor(options?: SketchType.Page); 35 | constructor(options?: any) { 36 | this.do_objectID = (options && options.do_objectID) || v4().toUpperCase(); 37 | this.name = (options && options.name) || "Page"; 38 | this.frame = (options && options.frame) || new Rect().toSketchJSON(); 39 | this.layers = (options && options.layers) || []; 40 | 41 | this.booleanOperation = (options && options.booleanOperation) || -1; 42 | this.exportOptions = 43 | (options && options.exportOptions) || INIT_DATA.ExportOptions; 44 | this.isFixedToViewport = (options && options.isFixedToViewport) || false; 45 | this.isFlippedHorizontal = 46 | (options && options.isFlippedHorizontal) || false; 47 | this.isFlippedVertical = (options && options.isFlippedVertical) || false; 48 | this.isLocked = (options && options.isLocked) || false; 49 | this.isVisible = (options && options.isVisible) || true; 50 | this.layerListExpandedType = 51 | (options && options.layerListExpandedType) || 0; 52 | this.nameIsFixed = (options && options.nameIsFixed) || false; 53 | this.resizingConstraint = (options && options.resizingConstraint) || 63; 54 | this.resizingType = (options && options.resizingType) || 0; 55 | this.rotation = (options && options.rotation) || 0; 56 | this.shouldBreakMaskChain = 57 | (options && options.shouldBreakMaskChain) || false; 58 | this.hasClickThrough = (options && options.hasClickThrough) || true; 59 | this.includeInCloudUpload = 60 | (options && options.includeInCloudUpload) || true; 61 | this.horizontalRulerData = 62 | (options && options.horizontalRulerData) || INIT_DATA.RulerData; 63 | this.verticalRulerData = 64 | (options && options.verticalRulerData) || INIT_DATA.RulerData; 65 | } 66 | 67 | updateProps(options?: SketchType.Page): void; 68 | updateProps(options?: any) { 69 | Object.keys(options).forEach((prop) => { 70 | if (this.hasOwnProperty(prop)) { 71 | this[prop as keyof this] = options[prop]; 72 | } 73 | }); 74 | } 75 | 76 | addSymbolMaster(symbolMaster: SketchType.SymbolMaster) { 77 | this.layers.push(symbolMaster); 78 | } 79 | 80 | getPageId() { 81 | return this.do_objectID; 82 | } 83 | 84 | getName() { 85 | return this.name; 86 | } 87 | 88 | static fromData(options: SketchType.Page): Page { 89 | const page = new this(); 90 | page.updateProps(options); 91 | return page; 92 | } 93 | 94 | static fromPath(path: string): Page { 95 | const file = fs.readFileSync(path, "utf-8"); 96 | if (file) { 97 | const options: SketchType.Page = JSON.parse(file); 98 | const page = new this(options); 99 | return page; 100 | } else { 101 | throw Error("Invalid data from path."); 102 | } 103 | } 104 | 105 | artboards(): SketchType.ArtboardLike[] { 106 | const allArtboards: SketchType.ArtboardLike[] = []; 107 | const layers = this.layers; 108 | layers.forEach((layer) => { 109 | if (layer._class === "artboard" || layer._class === "symbolMaster") { 110 | allArtboards.push(layer); 111 | } 112 | }); 113 | return allArtboards; 114 | } 115 | 116 | symbolMasters(): SketchType.SymbolMaster[] { 117 | const allSymbolMasters: SketchType.SymbolMaster[] = []; 118 | const layers = this.layers; 119 | layers.forEach((layer) => { 120 | if (layer._class === "symbolMaster") { 121 | allSymbolMasters.push(layer); 122 | } 123 | }); 124 | return allSymbolMasters; 125 | } 126 | 127 | toSketchJSON(): SketchType.Page { 128 | return { 129 | _class: Page._class, 130 | 131 | do_objectID: this.do_objectID, 132 | name: this.name, 133 | layers: this.layers as SketchType.Layer[], 134 | 135 | booleanOperation: this.booleanOperation, 136 | exportOptions: this.exportOptions, 137 | frame: this.frame, 138 | isFixedToViewport: this.isFixedToViewport, 139 | isFlippedHorizontal: this.isFlippedHorizontal, 140 | isFlippedVertical: this.isFlippedVertical, 141 | isLocked: this.isLocked, 142 | isVisible: this.isVisible, 143 | layerListExpandedType: this.layerListExpandedType, 144 | nameIsFixed: this.nameIsFixed, 145 | resizingConstraint: this.resizingConstraint, 146 | resizingType: this.resizingType, 147 | rotation: this.rotation, 148 | shouldBreakMaskChain: this.shouldBreakMaskChain, 149 | hasClickThrough: this.hasClickThrough, 150 | includeInCloudUpload: this.includeInCloudUpload, 151 | horizontalRulerData: this.horizontalRulerData, 152 | verticalRulerData: this.verticalRulerData, 153 | }; 154 | } 155 | 156 | getLayers(options?: { 157 | classes: SketchType.LayerClass[]; 158 | }): SketchType.Layer[] { 159 | if (!options || !options.classes) { 160 | return this.layers; 161 | } 162 | 163 | const filteredLayerClasses = options.classes.map((c) => c.toLowerCase()); 164 | 165 | const filteredLayers: SketchType.Layer[] = []; 166 | const layers = this.layers; 167 | layers.forEach((layer) => { 168 | if (filteredLayerClasses.includes(layer._class.toLowerCase())) { 169 | filteredLayers.push(layer); 170 | } 171 | }); 172 | return filteredLayers; 173 | } 174 | 175 | reLayoutLayers() { 176 | let yMark = 0; 177 | const Y_Margin = 20; 178 | const Y_MAX = 2000; 179 | 180 | let xMark = 0; 181 | const X_Margin = 100; 182 | let localMaxWidth = 0; 183 | 184 | this.layers.forEach((layer, i) => { 185 | const { height, width } = layer.frame; 186 | 187 | if (width > localMaxWidth) { 188 | localMaxWidth = width; 189 | } 190 | 191 | let layerTop = yMark + Y_Margin; 192 | let layerBottom = layerTop + height; 193 | 194 | if (layerBottom > Y_MAX) { 195 | layerTop = 0; 196 | layerBottom = height; 197 | xMark += localMaxWidth + X_Margin; 198 | localMaxWidth = width; 199 | } 200 | 201 | this.layers[i].frame.y = layerTop; 202 | yMark = layerBottom; 203 | 204 | this.layers[i].frame.x = xMark; 205 | }); 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /src/structures/SketchFile.ts: -------------------------------------------------------------------------------- 1 | import * as fse from "fs-extra"; 2 | import { exec } from "child_process"; 3 | import { promisify } from "util"; 4 | import { zip } from "compressing"; 5 | 6 | export interface UnzipOpts { 7 | cli?: boolean; 8 | } 9 | 10 | export class SketchFile { 11 | path: string; 12 | 13 | constructor(path: string) { 14 | this.path = path; 15 | } 16 | 17 | async unzip(packPath: string, options?: UnzipOpts): Promise { 18 | const fileExists = await fse.pathExists(this.path); 19 | if (!fileExists) { 20 | throw new Error("sketch file not found!"); 21 | } 22 | 23 | fse.ensureDir(packPath); 24 | 25 | if (options?.cli) { 26 | const execAsync = promisify(exec); 27 | await execAsync(`unzip -o ${this.path} -d ${packPath}`); 28 | } else { 29 | await zip.uncompress(this.path, packPath); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/structures/User.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | 3 | import SketchType from "../types"; 4 | import { CONSTANTS } from "../constants"; 5 | 6 | export class User { 7 | document: SketchType.UserDocumentConfigs; 8 | 9 | otherConfigs?: { key: string; value: SketchType.UserPageConfigs | any }[]; 10 | 11 | constructor(); 12 | constructor(options: SketchType.User); 13 | constructor(options?: any) { 14 | this.document = (options && options.document) || { 15 | pageListCollapsed: CONSTANTS.user.document.pageListCollapsed.default, 16 | pageListHeight: CONSTANTS.user.document.pageListHeight.default, 17 | }; 18 | 19 | if (options) { 20 | Object.keys(options).forEach((key) => { 21 | if (key !== "document") { 22 | if (!this.otherConfigs) this.otherConfigs = []; 23 | this.otherConfigs.push({ 24 | key, 25 | value: options[key], 26 | }); 27 | } 28 | }); 29 | } 30 | } 31 | 32 | updateProps(options?: SketchType.User): void; 33 | updateProps(options?: any) { 34 | Object.keys(options).forEach((prop) => { 35 | if (this.hasOwnProperty(prop)) { 36 | this[prop as keyof this] = options[prop]; 37 | } 38 | }); 39 | } 40 | 41 | static fromData(options: SketchType.User): User { 42 | return new this(options); 43 | } 44 | 45 | static fromPath(path: string): User { 46 | const file = fs.readFileSync(path, "utf-8"); 47 | if (file) { 48 | const meta = new this(JSON.parse(file)); 49 | return meta; 50 | } else { 51 | throw Error("Invalid data from path."); 52 | } 53 | } 54 | 55 | toSketchJSON(): SketchType.User { 56 | const json: SketchType.User = { document: this.document }; 57 | 58 | if (this.otherConfigs) { 59 | this.otherConfigs.forEach(({ key, value }) => { 60 | json[key] = value; 61 | }); 62 | } 63 | 64 | return json; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/structures/models/PagesAndArtboards.ts: -------------------------------------------------------------------------------- 1 | import SketchType from "../../types"; 2 | import { Page } from "../Page"; 3 | 4 | export class PagesAndArtboards { 5 | data: SketchType.PagesAndArtboards; 6 | 7 | constructor(data?: SketchType.PagesAndArtboards) { 8 | if (data) { 9 | this.data = data; 10 | } else { 11 | const page = new Page(); 12 | const data = {} as SketchType.PagesAndArtboards; 13 | data[page.getPageId()] = { 14 | name: page.getName(), 15 | artboards: {}, 16 | }; 17 | 18 | this.data = data; 19 | } 20 | } 21 | 22 | static fromPages(pages: Page[]): PagesAndArtboards { 23 | const data = {} as SketchType.PagesAndArtboards; 24 | 25 | pages.forEach((page) => { 26 | const pageId = page.getPageId(); 27 | const pageName = page.getName(); 28 | 29 | const pageRecord = {} as SketchType.PageRecord; 30 | pageRecord.name = pageName; 31 | pageRecord.artboards = {}; 32 | 33 | page.artboards().forEach((artboard) => { 34 | pageRecord.artboards[artboard.do_objectID] = { 35 | name: artboard.name, 36 | }; 37 | }); 38 | 39 | data[pageId] = pageRecord; 40 | }); 41 | 42 | const pagesAndArtboards = new PagesAndArtboards(data); 43 | 44 | return pagesAndArtboards; 45 | } 46 | 47 | toSketchJSON(): SketchType.PagesAndArtboards { 48 | return this.data; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/structures/models/Rect.ts: -------------------------------------------------------------------------------- 1 | import SketchType from "../../types"; 2 | 3 | export class Rect { 4 | static _class: "rect" = "rect"; 5 | 6 | constrainProportions: boolean; 7 | height: number; 8 | width: number; 9 | x: number; 10 | y: number; 11 | 12 | constructor(); 13 | constructor(options: SketchType.Rect); 14 | constructor(options?: any) { 15 | this.constrainProportions = 16 | (options && options.constrainProportions) || false; 17 | this.height = (options && options.height) || 100; 18 | this.width = (options && options.width) || 100; 19 | this.x = (options && options.x) || 0; 20 | this.y = (options && options.y) || 0; 21 | } 22 | 23 | toSketchJSON(): SketchType.Rect { 24 | return { 25 | _class: Rect._class, 26 | constrainProportions: this.constrainProportions, 27 | height: this.height, 28 | width: this.width, 29 | x: this.x, 30 | y: this.y, 31 | }; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/structures/models/Style.ts: -------------------------------------------------------------------------------- 1 | import { v4 } from "uuid"; 2 | import SketchType, { Uuid } from "../../types"; 3 | import { INIT_DATA } from "../../constants"; 4 | 5 | export interface StyleConstrOpts { 6 | do_objectID?: Uuid; 7 | startMarkerType?: SketchType.MarkerType; 8 | endMarkerType?: SketchType.MarkerType; 9 | miterLimit?: number; 10 | windingRule?: number; 11 | borders?: SketchType.Border[]; 12 | borderOptions?: SketchType.BorderOptions; 13 | fills?: SketchType.Fill[]; 14 | shadows?: SketchType.Shadow[]; 15 | innerShadows?: SketchType.InnerShadow[]; 16 | textStyle?: SketchType.TextStyle; 17 | contextSettings?: SketchType.GraphicsContextSettings; 18 | colorControls?: SketchType.ColorControls; 19 | } 20 | 21 | export class Style { 22 | static _class: "style" = "style"; 23 | 24 | do_objectID: Uuid; 25 | 26 | startMarkerType: SketchType.MarkerType; 27 | endMarkerType: SketchType.MarkerType; 28 | miterLimit: number; 29 | windingRule: number; 30 | borders: SketchType.Border[]; 31 | borderOptions: SketchType.BorderOptions; 32 | fills: SketchType.Fill[]; 33 | shadows: SketchType.Shadow[]; 34 | innerShadows: SketchType.InnerShadow[]; 35 | textStyle: SketchType.TextStyle; 36 | contextSettings: SketchType.GraphicsContextSettings; 37 | colorControls: SketchType.ColorControls; 38 | 39 | constructor(options?: StyleConstrOpts) { 40 | this.do_objectID = (options && options.do_objectID) || v4().toUpperCase(); 41 | 42 | this.startMarkerType = (options && options.startMarkerType) || 0; 43 | this.endMarkerType = (options && options.endMarkerType) || 0; 44 | this.miterLimit = (options && options.miterLimit) || 10; 45 | this.windingRule = (options && options.windingRule) || 0; 46 | this.borders = (options && options.borders) || []; 47 | this.borderOptions = 48 | (options && options.borderOptions) || INIT_DATA.BorderOptions; 49 | this.fills = (options && options.fills) || []; 50 | this.shadows = (options && options.shadows) || []; 51 | this.innerShadows = (options && options.innerShadows) || []; 52 | this.textStyle = (options && options.textStyle) || INIT_DATA.TextStyle; 53 | this.contextSettings = 54 | (options && options.contextSettings) || INIT_DATA.GraphicsContextSettings; 55 | this.colorControls = 56 | (options && options.colorControls) || INIT_DATA.ColorControls; 57 | } 58 | 59 | toSketchJSON(): SketchType.Style { 60 | return { 61 | _class: Style._class, 62 | do_objectID: this.do_objectID, 63 | startMarkerType: this.startMarkerType, 64 | endMarkerType: this.endMarkerType, 65 | miterLimit: this.miterLimit, 66 | windingRule: this.windingRule, 67 | borders: this.borders, 68 | borderOptions: this.borderOptions, 69 | fills: this.fills, 70 | shadows: this.shadows, 71 | innerShadows: this.innerShadows, 72 | textStyle: this.textStyle, 73 | contextSettings: this.contextSettings, 74 | colorControls: this.colorControls, 75 | }; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/structures/models/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Rect"; 2 | export * from "./Style"; 3 | -------------------------------------------------------------------------------- /src/types/document.ts: -------------------------------------------------------------------------------- 1 | import FileFormat from "@sketch-hq/sketch-file-format-ts"; 2 | 3 | export type Document = FileFormat.Document; 4 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | export * as FileFormat from "@sketch-hq/sketch-file-format-ts"; 2 | 3 | export * from "./utils"; 4 | export * from "./origin"; 5 | export * from "./user"; 6 | export * from "./meta"; 7 | export * from "./document"; 8 | export * from "./page"; 9 | 10 | export type JSONPackComponent = 11 | | "user" 12 | | "meta" 13 | | "document" 14 | | "pages" 15 | | "images"; 16 | 17 | import * as SketchType from "."; 18 | export default SketchType; 19 | -------------------------------------------------------------------------------- /src/types/meta.ts: -------------------------------------------------------------------------------- 1 | import FileFormat from "@sketch-hq/sketch-file-format-ts"; 2 | 3 | export type Meta = FileFormat.Meta; 4 | -------------------------------------------------------------------------------- /src/types/origin.ts: -------------------------------------------------------------------------------- 1 | // Formatted and synced by sync-origin-type 2 | import FileFormat from "@sketch-hq/sketch-file-format-ts"; 3 | import { tuple } from "./utils"; 4 | 5 | export const ORIGIN_TYPES = tuple( 6 | "AnyLayer", 7 | "Artboard", 8 | "AssetCollection", 9 | "Bitmap", 10 | "BooleanOperation", 11 | "Border", 12 | "BorderOptions", 13 | "BundleId", 14 | "Color", 15 | "ColorControls", 16 | "ColorSpace", 17 | "ExportOptions", 18 | "FileRef", 19 | "Fill", 20 | "ForeignLayerStyle", 21 | "ForeignSymbol", 22 | "ForeignTextStyle", 23 | "GraphicsContextSettings", 24 | "Group", 25 | "Hotspot", 26 | "InnerShadow", 27 | "LayerListExpanded", 28 | "MarkerType", 29 | "NumericalBool", 30 | "Oval", 31 | "ParagraphStyle", 32 | "Polygon", 33 | "Rect", 34 | "Rectangle", 35 | "ResizeType", 36 | "RulerData", 37 | "Shadow", 38 | "ShapeGroup", 39 | "ShapePath", 40 | "SharedStyleContainer", 41 | "SharedTextStyleContainer", 42 | "SketchVariant", 43 | "Slice", 44 | "Star", 45 | "Style", 46 | "SymbolInstance", 47 | "SymbolMaster", 48 | "Text", 49 | "TextStyle", 50 | "Triangle" 51 | ); 52 | export type OriginTypes = typeof ORIGIN_TYPES[number]; 53 | 54 | export type AnyLayer = FileFormat.AnyLayer; 55 | export type Artboard = FileFormat.Artboard; 56 | export type AssetCollection = FileFormat.AssetCollection; 57 | export type Bitmap = FileFormat.Bitmap; 58 | export type BooleanOperation = FileFormat.BooleanOperation; 59 | export type Border = FileFormat.Border; 60 | export type BorderOptions = FileFormat.BorderOptions; 61 | export type BundleId = FileFormat.BundleId; 62 | export type Color = FileFormat.Color; 63 | export type ColorControls = FileFormat.ColorControls; 64 | export type ColorSpace = FileFormat.ColorSpace; 65 | export type ExportOptions = FileFormat.ExportOptions; 66 | export type FileRef = FileFormat.FileRef; 67 | export type Fill = FileFormat.Fill; 68 | export type ForeignLayerStyle = FileFormat.ForeignLayerStyle; 69 | export type ForeignSymbol = FileFormat.ForeignSymbol; 70 | export type ForeignTextStyle = FileFormat.ForeignTextStyle; 71 | export type GraphicsContextSettings = FileFormat.GraphicsContextSettings; 72 | export type Group = FileFormat.Group; 73 | export type Hotspot = FileFormat.Hotspot; 74 | export type InnerShadow = FileFormat.InnerShadow; 75 | export type LayerListExpanded = FileFormat.LayerListExpanded; 76 | export type MarkerType = FileFormat.MarkerType; 77 | export type NumericalBool = FileFormat.NumericalBool; 78 | export type Oval = FileFormat.Oval; 79 | export type ParagraphStyle = FileFormat.ParagraphStyle; 80 | export type Polygon = FileFormat.Polygon; 81 | export type Rect = FileFormat.Rect; 82 | export type Rectangle = FileFormat.Rectangle; 83 | export type ResizeType = FileFormat.ResizeType; 84 | export type RulerData = FileFormat.RulerData; 85 | export type Shadow = FileFormat.Shadow; 86 | export type ShapeGroup = FileFormat.ShapeGroup; 87 | export type ShapePath = FileFormat.ShapePath; 88 | export type SharedStyleContainer = FileFormat.SharedStyleContainer; 89 | export type SharedTextStyleContainer = FileFormat.SharedTextStyleContainer; 90 | export type SketchVariant = FileFormat.SketchVariant; 91 | export type Slice = FileFormat.Slice; 92 | export type Star = FileFormat.Star; 93 | export type Style = FileFormat.Style; 94 | export type SymbolInstance = FileFormat.SymbolInstance; 95 | export type SymbolMaster = FileFormat.SymbolMaster; 96 | export type Text = FileFormat.Text; 97 | export type TextStyle = FileFormat.TextStyle; 98 | export type Triangle = FileFormat.Triangle; 99 | -------------------------------------------------------------------------------- /src/types/page.ts: -------------------------------------------------------------------------------- 1 | import FileFormat from "@sketch-hq/sketch-file-format-ts"; 2 | import { tuple } from "./utils"; 3 | import { 4 | Artboard, 5 | Group, 6 | Oval, 7 | Polygon, 8 | Rectangle, 9 | ShapePath, 10 | Star, 11 | Triangle, 12 | ShapeGroup, 13 | Text, 14 | SymbolMaster, 15 | SymbolInstance, 16 | Slice, 17 | Hotspot, 18 | Bitmap, 19 | } from "./origin"; 20 | 21 | export type Page = FileFormat.Page; 22 | 23 | export type ArtboardLike = Artboard | SymbolMaster; 24 | 25 | // Layer = AnyLayer - 'Page' 26 | export const LAYER_CLASS_OPTIONS = tuple( 27 | "Artboard", 28 | "Group", 29 | "Oval", 30 | "Polygon", 31 | "Rectangle", 32 | "ShapePath", 33 | "Star", 34 | "Triangle", 35 | "ShapeGroup", 36 | "Text", 37 | "SymbolMaster", 38 | "SymbolInstance", 39 | "Slice", 40 | "Hotspot", 41 | "Bitmap" 42 | ); 43 | export type LayerClass = typeof LAYER_CLASS_OPTIONS[number]; 44 | export type Layer = 45 | | Artboard 46 | | Group 47 | | Oval 48 | | Polygon 49 | | Rectangle 50 | | ShapePath 51 | | Star 52 | | Triangle 53 | | ShapeGroup 54 | | Text 55 | | SymbolMaster 56 | | SymbolInstance 57 | | Slice 58 | | Hotspot 59 | | Bitmap; 60 | -------------------------------------------------------------------------------- /src/types/user.ts: -------------------------------------------------------------------------------- 1 | import FileFormat from "@sketch-hq/sketch-file-format-ts"; 2 | import { CoordString } from "./utils"; 3 | 4 | export interface UserDocumentConfigs { 5 | pageListHeight: number; 6 | pageListCollapsed: FileFormat.NumericalBool; 7 | expandedSymbolPathsInSidebar?: []; 8 | expandedTextStylePathsInPopover?: []; 9 | libraryListCollapsed?: FileFormat.NumericalBool; 10 | componentSidebarTreeStructure?: FileFormat.NumericalBool; 11 | } 12 | 13 | export interface UserPageConfigs { 14 | scrollOrigin: CoordString; 15 | zoomValue?: number; 16 | } 17 | 18 | export type User = FileFormat.User; 19 | -------------------------------------------------------------------------------- /src/types/utils.ts: -------------------------------------------------------------------------------- 1 | export const tuple = (...args: T) => args; 2 | 3 | export interface CoordString { 4 | x: number; 5 | y: number; 6 | pattern: "{x, y}"; 7 | } 8 | 9 | export type Uuid = string; 10 | 11 | export interface PageRecord { 12 | name: string; 13 | artboards: { 14 | [key: string]: { 15 | name: string; 16 | }; 17 | }; 18 | } 19 | 20 | export type PagesAndArtboards = { 21 | [key: string]: PageRecord; 22 | }; 23 | 24 | export type Version = 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130; 25 | -------------------------------------------------------------------------------- /src/utils/fs-custom.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | import * as fse from "fs-extra"; 3 | import * as pat from "path"; 4 | 5 | /** 6 | * Custom verson of `fs.writeFileSync`, reset the path first. 7 | */ 8 | export function writeFileSync( 9 | path: string, 10 | data: string, 11 | options?: fs.WriteFileOptions 12 | ) { 13 | resetPathSync(path); 14 | fs.writeFileSync(path, data, options); 15 | } 16 | 17 | /** 18 | * Custom verson of `fse.writeFile`, reset the path first. 19 | */ 20 | export async function writeFile( 21 | path: string, 22 | data: string, 23 | options?: fse.WriteFileOptions 24 | ) { 25 | try { 26 | await resetPath(path); 27 | await fse.writeFile(path, data, options); 28 | } catch (error) { 29 | console.log(error); 30 | } 31 | } 32 | 33 | /** 34 | * Test whether or not the given directory or file exists. For files, remove it. For dirctories, remove it recursively and then ensure it again. 35 | */ 36 | export async function resetPath(path: string) { 37 | try { 38 | const pathExists = await fse.pathExists(path); 39 | 40 | if (pathExists) { 41 | await fse.remove(path); 42 | } 43 | 44 | if (isPathLikeDir(path)) { 45 | await fse.ensureDir(path); 46 | } else { 47 | await fse.ensureDir(pat.dirname(path)); 48 | } 49 | } catch (error) { 50 | console.log(error); 51 | } 52 | } 53 | 54 | /** 55 | * Test whether or not the given directory or file exists. For files, remove it. For dirctories, remove it recursively and then ensure it again. 56 | */ 57 | export function resetPathSync(path: string) { 58 | const pathExists = fse.existsSync(path); 59 | if (pathExists) { 60 | fse.removeSync(path); 61 | } 62 | 63 | if (isPathLikeDir(path)) { 64 | fse.ensureDirSync(path); 65 | } else { 66 | fse.ensureDirSync(pat.dirname(path)); 67 | } 68 | } 69 | 70 | /** 71 | * Check whether a path string is like a directory or not (more like a file). 72 | */ 73 | export function isPathLikeDir(path: string) { 74 | const obj = pat.parse(path); 75 | return !obj.ext || obj.ext === ""; 76 | } 77 | -------------------------------------------------------------------------------- /src/utils/image.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | import * as fse from "fs-extra"; 3 | import * as path from "path"; 4 | import { isObject } from "./types"; 5 | 6 | export function bitmap2base64Sync(file: string): string { 7 | const imageAsBase64Str = fs.readFileSync(file, "base64"); 8 | return imageAsBase64Str; 9 | } 10 | 11 | export async function bitmap2base64(file: string): Promise { 12 | const imageAsBase64Str = await fse.readFile(file, "base64"); 13 | return imageAsBase64Str; 14 | } 15 | 16 | export function imageIsBitmap(imageJSON: any): boolean { 17 | return ( 18 | imageJSON._ref_class === "MSImageData" && 19 | imageJSON._class === "MSJSONFileReference" && 20 | imageJSON._ref 21 | ); 22 | } 23 | 24 | export interface ImageInfo { 25 | name: string; 26 | base64: string; 27 | } 28 | 29 | export async function findAllBitmapInSketchJSON( 30 | sketchJSON: any, 31 | rootPath: string 32 | ) { 33 | const store: ImageInfo[] = []; 34 | 35 | if (isObject(sketchJSON)) { 36 | for (const key in sketchJSON) { 37 | if (key === "image" && imageIsBitmap(sketchJSON["image"])) { 38 | const pat = path.join(rootPath, sketchJSON["image"]._ref); 39 | const base64 = await bitmap2base64(pat); 40 | const name = sketchJSON["image"]._ref.replace("images/", ""); 41 | store.push({ name, base64 }); 42 | } else if (isObject(sketchJSON[key]) || Array.isArray(sketchJSON[key])) { 43 | const more = await findAllBitmapInSketchJSON(sketchJSON[key], rootPath); 44 | store.push(...more); 45 | } 46 | } 47 | } else if (Array.isArray(sketchJSON)) { 48 | for (let i = 0; i < sketchJSON.length; i++) { 49 | const element = sketchJSON[i]; 50 | const more = await findAllBitmapInSketchJSON(element, rootPath); 51 | store.push(...more); 52 | } 53 | } 54 | 55 | return store; 56 | } 57 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | import SketchType from "../types"; 2 | 3 | /** 4 | * @deprecated use markLayer instead 5 | */ 6 | export function labelSymbolMaster( 7 | symbolMaster: SketchType.SymbolMaster, 8 | labelObject: Record 9 | ): SketchType.SymbolMaster { 10 | symbolMaster.userInfo = labelObject; 11 | return symbolMaster; 12 | } 13 | 14 | export function markLayer( 15 | layer: SketchType.AnyLayer, 16 | markName: string, 17 | markObject: Record 18 | ) { 19 | if (!layer.userInfo) { 20 | layer.userInfo = {}; 21 | } 22 | 23 | if (layer.userInfo[markName]) { 24 | console.warn(`Overwriting userInfo.${markName} of layer: ${layer}`); 25 | } 26 | 27 | layer.userInfo[markName] = markObject; 28 | return layer; 29 | } 30 | 31 | export function makeid(length: number): string { 32 | var result = ""; 33 | var characters = "abcdefghijklmnopqrstuvwxyz0123456789"; 34 | var charactersLength = characters.length; 35 | for (var i = 0; i < length; i++) { 36 | result += characters.charAt(Math.floor(Math.random() * charactersLength)); 37 | } 38 | return result; 39 | } 40 | 41 | export * from "./fs-custom"; 42 | export * from "./object"; 43 | -------------------------------------------------------------------------------- /src/utils/object.ts: -------------------------------------------------------------------------------- 1 | export function isObject(item: any) { 2 | return item && typeof item === "object" && !Array.isArray(item); 3 | } 4 | 5 | export function assignDeep(target: any, ...sources: any): any { 6 | if (!sources.length) return target; 7 | const source = sources.shift(); 8 | 9 | if (isObject(target) && isObject(source)) { 10 | for (const key in source) { 11 | if (isObject(source[key])) { 12 | if (!target[key]) { 13 | Object.assign(target, { [key]: {} }); 14 | } else { 15 | target[key] = Object.assign({}, target[key]); 16 | } 17 | assignDeep(target[key], source[key]); 18 | } else { 19 | Object.assign(target, { [key]: source[key] }); 20 | } 21 | } 22 | } 23 | 24 | return assignDeep(target, ...sources); 25 | } 26 | -------------------------------------------------------------------------------- /src/utils/types.ts: -------------------------------------------------------------------------------- 1 | export function isObject(item: any) { 2 | return item && typeof item === "object" && !Array.isArray(item); 3 | } 4 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "declaration": true, 6 | "outDir": "./dist", 7 | "strict": true 8 | }, 9 | "exclude": ["dist", "node_modules", "temp", "__test__", "**/*.test.ts"] 10 | } 11 | --------------------------------------------------------------------------------