├── .babelrc ├── .eslintrc ├── .gitignore ├── Makefile ├── Readme.md ├── package.json └── src ├── NS ├── blendMode.js ├── blur.js ├── border.js ├── color.js ├── fill.js ├── font.js ├── kern.js ├── shadows.js ├── spacing.js └── textDecoration.js ├── exporter.js ├── importer.js ├── index.js ├── jsonPath.js ├── layer.js ├── layers ├── artboardLayer.js ├── general.js ├── groupLayer.js ├── imageLayer.js ├── pageLayer.js ├── pathLayer.js ├── shapeGroupLayer.js ├── symbolLayer.js ├── symbolMasterLayer.js ├── textLayer.js └── types.js ├── sharedStyle.js ├── sharedTextStyle.js └── util.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | presets: [[ 3 | "es2015", 4 | { 5 | modules: false 6 | } 7 | ], "stage-0"] 8 | } 9 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | --- 2 | extends: 3 | - standard 4 | - plugin:import/warnings 5 | - plugin:import/errors 6 | - sketch 7 | parserOptions: 8 | ecmaVersion: 2017 9 | sourceType: module 10 | ecmaFeatures: 11 | jsx: true 12 | experimentalObjectRestSpread: true 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # build artefacts 2 | build 3 | 4 | # npm 5 | node_modules 6 | 7 | # Logs 8 | logs 9 | *.log 10 | npm-debug.log* 11 | 12 | # Optional npm cache directory 13 | .npm 14 | 15 | # Optional REPL history 16 | .node_repl_history 17 | 18 | # Optional Env variables 19 | .env 20 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | BUILD_DIST ?= build 2 | BUILD_TARGET ?= src/ 3 | BUILD_FLAGS ?= --out-dir $(BUILD_DIST) 4 | 5 | TEST_TARGET ?= tests/ 6 | TEST_FLAGS ?= --require babel-register 7 | 8 | include node_modules/@mathieudutour/js-fatigue/Makefile 9 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # DEPRECATED - See [Kactus](https://github.com/kactus-io/kactus) for a working version of this 2 | 3 | 4 | # sketch-module-json-sync 5 | 6 | `sketch-module-json-sync` is sketch module to export and import a sketch file to json. 7 | 8 | :baby_chick: experimental project 9 | 10 | ## Installation 11 | 12 | ```sh 13 | npm i -S sketch-module-json-sync 14 | ``` 15 | 16 | ## Usage 17 | 18 | ```js 19 | import { importFromJSON, exportToJSON } from 'sketch-module-json-sync' 20 | 21 | export default function (context) { 22 | exportToJSON(context) 23 | importFromJSON(context) 24 | } 25 | ``` 26 | 27 | ## TODO 28 | 29 | - [ ] allow to choose directory when no sketch file opened 30 | 31 | ## License 32 | MIT 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sketch-module-json-sync", 3 | "version": "0.2.1", 4 | "description": "A sketch module to export and import a sketch file to json", 5 | "main": "build/index.js", 6 | "files": [ 7 | "build" 8 | ], 9 | "scripts": { 10 | "prepublish": "make build", 11 | "test": "make test" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/mathieudutour/sketch-module-json-sync.git" 16 | }, 17 | "author": "Mathieu Dutour (http://mathieu.dutour.me/)", 18 | "license": "MIT", 19 | "bugs": { 20 | "url": "https://github.com/mathieudutour/sketch-module-json-sync/issues" 21 | }, 22 | "homepage": "https://github.com/mathieudutour/sketch-module-json-sync#readme", 23 | "devDependencies": { 24 | "@mathieudutour/js-fatigue": "^1.0.7", 25 | "eslint-config-sketch": "^0.1.2" 26 | }, 27 | "dependencies": { 28 | "sketch-module-fs": "^0.1.2", 29 | "to-camel-case": "^1.0.0" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/NS/blendMode.js: -------------------------------------------------------------------------------- 1 | const blendModeMap = { 2 | 0: 'normal', 3 | 1: 'darken', 4 | 2: 'multiply', 5 | 3: 'colorBurn', 6 | 4: 'lighten', 7 | 5: 'screen', 8 | 6: 'colorDodge', 9 | 7: 'overlay', 10 | 8: 'softLight', 11 | 9: 'hardLight', 12 | 10: 'difference', 13 | 11: 'exclusion', 14 | 12: 'hue', 15 | 13: 'saturation', 16 | 14: 'color', 17 | 15: 'lumiosity' 18 | } 19 | 20 | function blendModeNumberToString (num) { 21 | if (blendModeMap[num]) { 22 | return blendModeMap[num] 23 | } 24 | throw new Error('Unknow blendMode type. type=' + num) 25 | } 26 | 27 | function blendModeToNumber (str) { 28 | var keys = Object.keys(blendModeMap) 29 | for (var i = 0; i < keys.length; i++) { 30 | var key = keys[i] 31 | if (blendModeMap[key] === str) { 32 | return i 33 | } 34 | } 35 | throw new Error('Unknow blendMode type. type=' + str) 36 | } 37 | 38 | export function exportBlendMode (blendMode) { 39 | return blendModeNumberToString(blendMode) 40 | } 41 | 42 | export function importBlendMode (string) { 43 | return blendModeToNumber(string) 44 | } 45 | -------------------------------------------------------------------------------- /src/NS/blur.js: -------------------------------------------------------------------------------- 1 | import { round } from '../util' 2 | 3 | const blurMap = { 4 | 0: 'gaussian', 5 | 1: 'motion', 6 | 2: 'zoom', 7 | 3: 'background' 8 | } 9 | 10 | function blurNumberToString (num) { 11 | if (blurMap[num]) { 12 | return blurMap[num] 13 | } 14 | throw new Error('Unknow blur type. type=' + num) 15 | } 16 | 17 | function blurToNumber (str) { 18 | var keys = Object.keys(blurMap) 19 | for (var i = 0; i < keys.length; i++) { 20 | var key = keys[i] 21 | if (blurMap[key] === str) { 22 | return parseInt(key) 23 | } 24 | } 25 | throw new Error('Unknow blur type. type=' + str) 26 | } 27 | 28 | export function exportBlur (blur) { 29 | let s = { 30 | type: blurNumberToString(blur.type()), 31 | radius: round(blur.radius()) 32 | } 33 | 34 | if (blur.type() == 1) { // motion 35 | s.angle = round(blur.motionAngle()) 36 | } else if (blur.type() == 2) { // background 37 | s.center = { 38 | x: round(blur.center().x), 39 | y: round(blur.center().y) 40 | } 41 | } 42 | return s 43 | } 44 | 45 | export function importBlur (blur) { 46 | const b = MSStyleBlur.alloc().init() 47 | b.type = blurToNumber(blur.type) 48 | b.radius = blur.radius 49 | b.isEnabled = true 50 | if (blur.type === 'motion') { 51 | b.motionAngle = blur.angle 52 | } else if (blur.type === 'zoom') { 53 | b.center().x = blur.center.x 54 | b.center().y = blur.center.y 55 | } 56 | return b 57 | } 58 | -------------------------------------------------------------------------------- /src/NS/border.js: -------------------------------------------------------------------------------- 1 | import { exportFills, importFills } from './fill' 2 | 3 | var positionMap = { 4 | 0: 'center', 5 | 1: 'inside', 6 | 2: 'outside' 7 | } 8 | 9 | function positionNumberToString (num) { 10 | if (positionMap[num]) { 11 | return positionMap[num] 12 | } 13 | throw new Error('Unknow position type. type=' + num) 14 | } 15 | 16 | function positionToNumber (str) { 17 | var keys = Object.keys(positionMap) 18 | for (var i = 0; i < keys.length; i++) { 19 | var key = keys[i] 20 | if (positionMap[key] === str) { 21 | return parseInt(key) 22 | } 23 | } 24 | throw new Error('Unknow position type. type=' + str) 25 | } 26 | 27 | export function exportBorders (borders) { 28 | return exportFills(borders).map((b, i) => { 29 | return { 30 | ...b, 31 | position: positionNumberToString(borders[i].position()), 32 | thickness: borders[i].thickness() 33 | } 34 | }) 35 | } 36 | 37 | export function importBorders (borders) { 38 | return importFills(borders, '', () => MSStyleBorder.alloc().init()).map((b, i) => { 39 | b.position = positionToNumber(borders[i].position) 40 | b.thickness = borders[i].thickness 41 | return b 42 | }) 43 | } 44 | -------------------------------------------------------------------------------- /src/NS/color.js: -------------------------------------------------------------------------------- 1 | export function exportColor (nscolor) { 2 | if (nscolor.svgRepresentation) { 3 | return '' + nscolor.svgRepresentation() 4 | } 5 | let c = {} 6 | if (nscolor.RGBADictionary) { 7 | c = nscolor.RGBADictionary() 8 | } else { 9 | c.r = nscolor.redComponent() 10 | c.g = nscolor.greenComponent() 11 | c.b = nscolor.blueComponent() 12 | c.a = nscolor.alphaComponent() 13 | } 14 | const color = MSImmutableColor.colorWithRed_green_blue_alpha(c.r, c.g, c.b, c.a) 15 | return '' + color.stringValueWithAlpha(true) 16 | } 17 | 18 | export function importColor (string) { 19 | return MSImmutableColor.colorWithSVGString(string) 20 | } 21 | -------------------------------------------------------------------------------- /src/NS/fill.js: -------------------------------------------------------------------------------- 1 | import { findImage, toArray, imageName, round } from '../util' 2 | import { exportColor, importColor } from './color' 3 | import { exportBlendMode, importBlendMode } from './blendMode' 4 | 5 | var fillMap = { 6 | 0: 'color', 7 | 1: 'gradient', 8 | 4: 'image', 9 | 5: 'noise' 10 | } 11 | 12 | function fillNumberToString (num) { 13 | if (fillMap[num]) { 14 | return fillMap[num] 15 | } 16 | throw new Error('Unknow fill type. type=' + num) 17 | } 18 | 19 | function fillToNumber (str) { 20 | var keys = Object.keys(fillMap) 21 | for (var i = 0; i < keys.length; i++) { 22 | var key = keys[i] 23 | if (fillMap[key] === str) { 24 | return parseInt(key) 25 | } 26 | } 27 | throw new Error('Unknow fill type. type=' + str) 28 | } 29 | 30 | var gradientMap = { 31 | 0: 'linear', 32 | 1: 'radial', 33 | 2: 'angular' 34 | } 35 | 36 | function gradientNumberToString (num) { 37 | if (gradientMap[num]) { 38 | return gradientMap[num] 39 | } 40 | throw new Error('Unknow gradient type. type=' + num) 41 | } 42 | 43 | function gradientToNumber (str) { 44 | var keys = Object.keys(gradientMap) 45 | for (var i = 0; i < keys.length; i++) { 46 | var key = keys[i] 47 | if (gradientMap[key] === str) { 48 | return parseInt(key) 49 | } 50 | } 51 | throw new Error('Unknow gradient type. type=' + str) 52 | } 53 | 54 | function getGradientObject (gradient) { 55 | var from = gradient.from() 56 | var to = gradient.to() 57 | 58 | return { 59 | gradientType: gradientNumberToString(gradient.gradientType()), 60 | elipseLength: gradient.elipseLength(), 61 | from: { 62 | x: round(from.x), 63 | y: round(from.y) 64 | }, 65 | to: { 66 | x: round(to.x), 67 | y: round(to.y) 68 | }, 69 | stops: toArray(gradient.stops()).map((s) => { 70 | return { 71 | color: exportColor(s.color()), 72 | position: s.position() 73 | } 74 | }) 75 | } 76 | } 77 | 78 | export function exportFills (fills, savedImages) { 79 | const backgrounds = [] 80 | for (var i = 0; i < fills.length; i++) { 81 | let s = { 82 | type: fillNumberToString(fills[i].fillType()) 83 | } 84 | 85 | if (fills[i].fillType() == 0) { // color fill 86 | s.color = exportColor(fills[i].color()) 87 | } else if (fills[i].fillType() == 1) { // gradient 88 | s = { 89 | ...s, 90 | ...getGradientObject(fills[i].gradient()) 91 | } 92 | } else if (fills[i].fillType() == 4) { // image 93 | var image = findImage(savedImages, fills[i].image()) 94 | s.image = imageName(image) 95 | } 96 | 97 | var fillStyle = fills[i].contextSettings() 98 | var blendMode = fillStyle.blendMode() 99 | var opacity = fillStyle.opacity() 100 | 101 | if (blendMode > 0) { 102 | s.blendMode = exportBlendMode(blendMode) 103 | } 104 | 105 | if (opacity != 1) { 106 | s.opacity = opacity 107 | } 108 | 109 | if (!fills[i].isEnabled()) { 110 | s.enabled = false 111 | } 112 | 113 | backgrounds.push(s) 114 | } 115 | return backgrounds 116 | } 117 | 118 | export function importFills (backgrounds, path, primitive = () => MSStyleFill.alloc().init()) { 119 | return backgrounds.map(background => { 120 | const fill = primitive() 121 | fill.fillType = fillToNumber(background.type) 122 | switch (background.type) { 123 | case 'color': { 124 | fill.color = importColor(background.color) 125 | break 126 | } 127 | case 'image': { 128 | var imagePath = path + '/' + background.image 129 | var image = NSImage.alloc().initWithContentsOfFile(imagePath) 130 | var imageData = MSImageData.alloc().initWithImage_convertColorSpace(image, false) 131 | fill.image = imageData 132 | break 133 | } 134 | case 'gradient': { 135 | const stops = background.stops.map(stop => { 136 | return MSGradientStop.alloc().initWithPosition_color(stop.position, importColor(stop.color)) 137 | }) 138 | const gradient = MSGradient.alloc().initBlankGradient() 139 | gradient.gradientType = gradientToNumber(background.gradientType) 140 | gradient.elipseLength = background.elipseLength 141 | gradient.shouldSmoothenOpacity = background.shouldSmoothenOpacity 142 | gradient.from = CGPointMake(background.from.x, background.from.y) 143 | gradient.to = CGPointMake(background.to.x, background.to.y) 144 | gradient.stops = stops 145 | fill.gradient = gradient 146 | break 147 | } 148 | } 149 | if (background.enabled === false) { 150 | fill.isEnabled = false 151 | } 152 | if (background.blendMode) { 153 | fill.contextSettings().blendMode = importBlendMode(background.blendMode) 154 | } 155 | if (background.opacity) { 156 | fill.contextSettings().opacity = background.opacity 157 | } 158 | 159 | return fill 160 | }) 161 | } 162 | -------------------------------------------------------------------------------- /src/NS/font.js: -------------------------------------------------------------------------------- 1 | export function exportFont (nsfont) { 2 | var fontFamily = String(nsfont.fontDescriptor().objectForKey(NSFontNameAttribute)) 3 | var fontSize = String(nsfont.fontDescriptor().objectForKey(NSFontSizeAttribute)) * 1 4 | return { 5 | fontFamily, 6 | fontSize 7 | } 8 | } 9 | 10 | export function importFont (s) { 11 | return NSFont.fontWithName_size(s.fontFamily, s.fontSize) 12 | } 13 | -------------------------------------------------------------------------------- /src/NS/kern.js: -------------------------------------------------------------------------------- 1 | import { round } from '../util' 2 | 3 | export function exportKern (nskern) { 4 | return round(nskern) 5 | } 6 | 7 | export function importKern (string) { 8 | return Number(string) 9 | } 10 | -------------------------------------------------------------------------------- /src/NS/shadows.js: -------------------------------------------------------------------------------- 1 | import { toArray, round } from '../util' 2 | import { exportColor, importColor } from './color' 3 | 4 | export function exportShadows (shadows, inner) { 5 | return toArray(shadows).map(shadow => { 6 | return { 7 | offsetX: round(shadow.offsetX()), 8 | offsetY: round(shadow.offsetY()), 9 | radius: round(shadow.blurRadius()), 10 | spread: round(shadow.spread()), 11 | color: exportColor(shadow.color()), 12 | ...!shadow.isEnabled() && {enabled: false}, 13 | ...inner && {inner: true} 14 | } 15 | }) 16 | } 17 | 18 | export function importShadows (shadows) { 19 | const innerShadows = [] 20 | const outerShadows = [] 21 | function createShadow (s, inner) { 22 | const shadow = inner 23 | ? MSStyleInnerShadow.alloc().init() 24 | : MSStyleShadow.alloc().init() 25 | shadow.offsetX = s.offsetX 26 | shadow.offsetY = s.offsetY 27 | shadow.blurRadius = s.radius 28 | shadow.spread = s.spread 29 | if (typeof s.enable !== 'undefined' && !s.enable) { 30 | shadow.isEnabled = false 31 | } else { 32 | shadow.isEnabled = true 33 | } 34 | shadow.color = importColor(s.color) 35 | return shadow 36 | } 37 | 38 | shadows.forEach(shadow => { 39 | if (shadow.inner) { 40 | innerShadows.push(createShadow(shadow, true)) 41 | } else { 42 | outerShadows.push(createShadow(shadow)) 43 | } 44 | }) 45 | 46 | return {innerShadows, outerShadows} 47 | } 48 | -------------------------------------------------------------------------------- /src/NS/spacing.js: -------------------------------------------------------------------------------- 1 | import { round } from '../util' 2 | 3 | const alignMap = { 4 | 0: 'left', 5 | 1: 'right', 6 | 2: 'center', 7 | 3: 'justified' 8 | } 9 | 10 | function alignNumberToString (num) { 11 | if (alignMap[num]) { 12 | return alignMap[num] 13 | } 14 | throw new Error('Unknow align type. type=' + num) 15 | } 16 | 17 | function alignToNumber (str) { 18 | var keys = Object.keys(alignMap) 19 | for (var i = 0; i < keys.length; i++) { 20 | var key = keys[i] 21 | if (alignMap[key] === str) { 22 | return parseInt(key) 23 | } 24 | } 25 | throw new Error('Unknow align type. type=' + str) 26 | } 27 | 28 | export function exportSpacing (nsparagraphstyle) { 29 | const re = {} 30 | if (nsparagraphstyle.paragraphSpacing && nsparagraphstyle.paragraphSpacing() > 0) { 31 | re.paragraphSpacing = round(nsparagraphstyle.paragraphSpacing()) 32 | } 33 | if (nsparagraphstyle.lineSpacing && nsparagraphstyle.lineSpacing() > 0) { 34 | re.lineHeight = round(nsparagraphstyle.lineSpacing()) 35 | } 36 | if (nsparagraphstyle.paragraphSpacingBefore && nsparagraphstyle.paragraphSpacingBefore() > 0) { 37 | re.paragraphSpacingBefore = round(nsparagraphstyle.paragraphSpacingBefore()) 38 | } 39 | if (nsparagraphstyle.headIndent && nsparagraphstyle.headIndent() > 0) { 40 | re.headIndent = round(nsparagraphstyle.headIndent()) 41 | } 42 | if (nsparagraphstyle.tailIndent && nsparagraphstyle.tailIndent() > 0) { 43 | re.tailIndent = round(nsparagraphstyle.tailIndent()) 44 | } 45 | re.textAlign = alignNumberToString(nsparagraphstyle.alignment()) 46 | return re 47 | } 48 | 49 | export function importSpacing (obj, s) { 50 | if (s.paragraphSpacing) { 51 | obj.paragraphStyle().setParagraphSpacing(Number(s.paragraphSpacing)) 52 | } 53 | if (s.lineHeight) { 54 | obj.lineHeight = Number(s.lineHeight) 55 | } 56 | if (s.paragraphSpacingBefore) { 57 | obj.paragraphStyle().setParagraphSpacingBefore(Number(s.paragraphSpacingBefore)) 58 | } 59 | if (s.headIndent) { 60 | obj.paragraphStyle().setHeadIndent(Number(s.headIndent)) 61 | } 62 | if (s.tailIndent) { 63 | obj.paragraphStyle().setTailIndent(Number(s.tailIndent)) 64 | } 65 | obj.textAlignment = alignToNumber(s.textAlign) 66 | } 67 | -------------------------------------------------------------------------------- /src/NS/textDecoration.js: -------------------------------------------------------------------------------- 1 | var textTransformMap = { 2 | 0: 'normal', 3 | 1: 'uppercase', 4 | 2: 'lowercase' 5 | } 6 | 7 | function textTransformNumberToString (num) { 8 | if (textTransformMap[num]) { 9 | return textTransformMap[num] 10 | } 11 | throw new Error('Unknow text-transform type. type=' + num) 12 | } 13 | 14 | function textTransformToNumber (str) { 15 | var keys = Object.keys(textTransformMap) 16 | for (var i = 0; i < keys.length; i++) { 17 | var key = keys[i] 18 | if (textTransformMap[key] === str) { 19 | return parseInt(key) 20 | } 21 | } 22 | throw new Error('Unknow text-transform type. type=' + str) 23 | } 24 | 25 | var underlineMap = { 26 | 0: 'normal', 27 | 1: 'underline', 28 | 9: 'double-underline' 29 | } 30 | 31 | function underlineNumberToString (num) { 32 | if (underlineMap[num]) { 33 | return underlineMap[num] 34 | } 35 | throw new Error('Unknow underline type. type=' + num) 36 | } 37 | 38 | function underlineToNumber (str) { 39 | var keys = Object.keys(underlineMap) 40 | for (var i = 0; i < keys.length; i++) { 41 | var key = keys[i] 42 | if (underlineMap[key] === str) { 43 | return parseInt(key) 44 | } 45 | } 46 | throw new Error('Unknow underline type. type=' + str) 47 | } 48 | 49 | export function exportTextDecoration (attrs) { 50 | const re = {} 51 | // Underline 52 | if (attrs.NSUnderline > 0) { 53 | re.textDecoration = underlineNumberToString(attrs.NSUnderline) 54 | } 55 | 56 | // Line through 57 | if (attrs.NSStrikethrough > 0) { 58 | re.textDecoration = 'line-through' 59 | } 60 | 61 | // Text transform 62 | if (attrs.MSAttributedStringTextTransformAttribute > 0) { 63 | re.textTransform = textTransformNumberToString(attrs.MSAttributedStringTextTransformAttribute) 64 | } 65 | return re 66 | } 67 | 68 | export function importTextDecoration (obj, s) { 69 | if (s.textDecoration == 'line-through') { 70 | obj.addAttribute_value('NSStrikethrough', 1) 71 | } else if (s.textDecoration) { 72 | obj.addAttribute_value('NSUnderline', underlineToNumber(s.textDecoration)) 73 | } 74 | if (s.textTransform) { 75 | obj.addAttribute_value('MSAttributedStringTextTransformAttribute', textTransformToNumber(s.textTransform)) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/exporter.js: -------------------------------------------------------------------------------- 1 | import fs from 'sketch-module-fs' 2 | import { getLayers } from './layer' 3 | import { imageName, shasum, getCurrentDirectory, getCurrentFileName } from './util' 4 | import { exportSharedTextStyle } from './sharedTextStyle' 5 | 6 | export default function (context, options = {}) { 7 | const doc = context.document 8 | const pagesPath = getCurrentDirectory(context) + '/' + (options.exportFolder || getCurrentFileName(context)) 9 | const sharedTextStylesPath = getCurrentDirectory(context) + '/' + (options.exportSharedFolder || 'shared') + '/text-styles' 10 | 11 | fs.rmdir(pagesPath) 12 | fs.rmdir(sharedTextStylesPath) 13 | 14 | // export shared text styles 15 | const sharedTextStyles = doc.documentData().layerTextStyles().objects() 16 | if (sharedTextStyles.length) { 17 | fs.mkdir(sharedTextStylesPath) 18 | } 19 | for (let i = 0; i < sharedTextStyles.length; i++) { 20 | exportSharedTextStyle(sharedTextStylesPath, sharedTextStyles[i], sharedTextStyles.length - i) 21 | } 22 | 23 | // export pages 24 | const pages = getLayers(doc.pages()) 25 | for (let i = 0; i < pages.length; i++) { 26 | exportLayer(pagesPath, pages[i], pages.length - i, {}) 27 | } 28 | } 29 | 30 | function exportLayer (parentPath, layer, index, parent) { 31 | if (['shapePath', 'oval', 'rectangle'].indexOf(parent.type) != -1 && (!layer.path || layer.path() === '')) { 32 | return 33 | } 34 | 35 | const path = parentPath + '/' + layer.dirName() 36 | fs.mkdir(path) 37 | 38 | saveImages(layer, path) 39 | 40 | const json = layer.exportJSON() 41 | json.index = index 42 | 43 | fs.writeFile(path + '/' + json.type + '.json', JSON.stringify(json, null, ' ')) 44 | 45 | const layers = getLayers(layer.layers()) 46 | layer.setLayers(layers) 47 | 48 | if (layers.length > 0) { 49 | for (let i = 0; i < layers.length; i++) { 50 | exportLayer(path, layers[i], layers.length - i, json) 51 | } 52 | } 53 | } 54 | 55 | function saveImages (layer, path) { 56 | const images = layer.images() 57 | images.forEach(image => { 58 | const fromPath = path + '/' + image.name 59 | fs.writeFile(fromPath, image.image) 60 | image.sha1 = shasum(fromPath) 61 | const toPath = path + '/' + imageName(image) 62 | fs.rename(fromPath, toPath) 63 | }) 64 | layer.setSavedImages(images) 65 | } 66 | -------------------------------------------------------------------------------- /src/importer.js: -------------------------------------------------------------------------------- 1 | import { jsonFilePaths, jsonTree } from './jsonPath' 2 | import { Layer } from './layer' 3 | import { getCurrentDirectory, getCurrentFileName } from './util' 4 | import { importSharedTextStyle } from './sharedTextStyle' 5 | 6 | export default function importer (context, options = {}) { 7 | const doc = context.document 8 | 9 | for (let i = 0; i < doc.pages().length; i++) { 10 | doc.removePage(doc.pages()[i]) 11 | } 12 | 13 | const lastPageToRemove = doc.pages()[0] 14 | 15 | // import shared text styles 16 | doc.documentData().layerTextStyles().removeAllSharedObjects() 17 | const sharedTextStylesPath = getCurrentDirectory(context) + '/' + (options.exportSharedFolder || 'shared') + '/text-styles' 18 | iterateOverJSONs(doc, sharedTextStylesPath, function (json, parent, current) { 19 | importSharedTextStyle(doc, json, parent, current) 20 | }) 21 | doc.reloadInspector() 22 | 23 | // import pages 24 | const pagesPath = getCurrentDirectory(context) + '/' + (options.exportFolder || getCurrentFileName(context)) 25 | iterateOverJSONs(doc, pagesPath, function (json, parent, current) { 26 | Layer(json.type) && Layer(json.type).importJSON(doc, json, parent, current) 27 | }) 28 | 29 | doc.removePage(lastPageToRemove) 30 | } 31 | 32 | function iterateOverJSONs (doc, path, func) { 33 | var jsons = jsonFilePaths(path) 34 | var tree = jsonTree(jsons, path) 35 | 36 | jsons.sort(compareJsonFilePath(tree)) 37 | 38 | for (var i = 0; i < jsons.length; i++) { 39 | var parent = parentPos(jsons[i], tree) 40 | var current = currentPos(jsons[i], tree) 41 | var json = current.json 42 | current.path = path + '/' + currentPath(jsons[i]) 43 | 44 | func(json, parent, current) 45 | } 46 | } 47 | 48 | function parentPos (path, tree) { 49 | var p = tree 50 | var components = path.pathComponents() 51 | for (var i = 0; i < (components.length - 2); i++) { 52 | var n = components[i] 53 | p = p[n] 54 | } 55 | if (p.jsonFileName) { 56 | return p 57 | } else { 58 | return null 59 | } 60 | } 61 | 62 | function currentPath (path) { 63 | var components = path.pathComponents() 64 | components.pop() 65 | return components.join('/') 66 | } 67 | 68 | function currentPos (path, tree) { 69 | var p = tree 70 | var components = path.pathComponents() 71 | for (var i = 0; i < components.length - 1; i++) { 72 | var n = components[i] 73 | p = p[n] 74 | } 75 | 76 | return p 77 | } 78 | 79 | function compareJsonFilePath (tree) { 80 | return function (a, b) { 81 | var as = a.pathComponents() 82 | var bs = b.pathComponents() 83 | var aLen = as.length 84 | var bLen = bs.length 85 | 86 | if (aLen == bLen && as.slice(0, -2).join('/') == bs.slice(0, -2).join('/')) { 87 | return currentPos(b, tree).json.index - currentPos(a, tree).json.index 88 | } else { 89 | return aLen - bLen 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import importer from './importer' 2 | import exporter from './exporter' 3 | 4 | export const exportToJSON = exporter 5 | export const importFromJSON = importer 6 | -------------------------------------------------------------------------------- /src/jsonPath.js: -------------------------------------------------------------------------------- 1 | import fs from 'sketch-module-fs' 2 | 3 | export function jsonFilePaths (path) { 4 | const ds = NSFileManager.defaultManager().enumeratorAtPath(path) 5 | let filename = ds && ds.nextObject && ds.nextObject() 6 | const paths = [] 7 | while (filename) { 8 | if (filename.pathExtension() == 'json') { 9 | paths.push(filename) 10 | } 11 | filename = ds.nextObject && ds.nextObject() 12 | } 13 | return paths 14 | } 15 | 16 | export function jsonTree (jsonPaths, path) { 17 | const tree = {} 18 | for (var i = 0; i < jsonPaths.length; i++) { 19 | const dirs = jsonPaths[i].pathComponents() 20 | let p = tree 21 | for (var j = 0; j < dirs.length; j++) { 22 | let n = dirs[j] 23 | if (n.pathExtension() == 'json') { 24 | n = 'jsonFileName' 25 | p[n] = dirs[j] 26 | 27 | const filePath = path + '/' + jsonPaths[i] 28 | const jsonString = fs.readFile(filePath) 29 | const json = JSON.parse(jsonString) 30 | p['json'] = json 31 | } else { 32 | p[n] = p[n] || {} 33 | } 34 | p = p[n] 35 | } 36 | } 37 | return tree 38 | } 39 | -------------------------------------------------------------------------------- /src/layer.js: -------------------------------------------------------------------------------- 1 | import * as types from './layers/types' 2 | 3 | import GeneralLayer from './layers/general' 4 | import PageLayer from './layers/pageLayer' 5 | import ArtboardLayer from './layers/artboardLayer' 6 | import TextLayer from './layers/textLayer' 7 | import ImageLayer from './layers/imageLayer' 8 | import ShapeGroupLayer from './layers/shapeGroupLayer' 9 | import PathLayer from './layers/pathLayer' 10 | import SymbolMasterLayer from './layers/symbolMasterLayer' 11 | import SymbolLayer from './layers/symbolLayer' 12 | import GroupLayer from './layers/groupLayer' 13 | 14 | export function getLayers (layers) { 15 | const re = [] 16 | for (let i = 0; i < layers.length; i++) { 17 | const type = types.getType(layers[i]) 18 | re.push(new (Layer(type))(layers[i])) 19 | } 20 | return re 21 | } 22 | 23 | export function Layer (type) { 24 | if (type === types.PAGE) { 25 | return PageLayer 26 | } else if (type === types.ARTBOARD) { 27 | return ArtboardLayer 28 | } else if (type === types.SYMBOL_MASTER) { 29 | return SymbolMasterLayer 30 | } else if (type === types.SYMBOL) { 31 | return SymbolLayer 32 | } else if (type === types.TEXT) { 33 | return TextLayer 34 | } else if (type === types.IMAGE) { 35 | return ImageLayer 36 | } else if ([types.OVAL, types.RECTANGLE, types.SHAPE_PATH, types.COMBINED_SHAPE].indexOf(type) !== -1) { 37 | return ShapeGroupLayer 38 | } else if (type === types.PATH) { 39 | return PathLayer 40 | } else if (type === types.GROUP) { 41 | return GroupLayer 42 | } else { 43 | return GeneralLayer 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/layers/artboardLayer.js: -------------------------------------------------------------------------------- 1 | import { importColor, exportColor } from '../NS/color' 2 | import GeneralLayer from './general' 3 | 4 | export default class ArtboardLayer extends GeneralLayer { 5 | styles () { 6 | return { 7 | ...super.styles(), 8 | ...this.artboardCssBackground() 9 | } 10 | } 11 | 12 | artboardCssBackground () { 13 | var re = {} 14 | 15 | if (this._layer.hasBackgroundColor()) { 16 | re.hasBackgroundColor = true 17 | re.backgroundColor = exportColor(this._layer.backgroundColor()) 18 | } 19 | 20 | return re 21 | } 22 | 23 | static importJSON (doc, json, parent, current) { 24 | var artboard = MSArtboardGroup.alloc().init() 25 | artboard.objectID = json.objectId 26 | artboard.setName(json.name) 27 | artboard.setRect(GeneralLayer.importBound(json)) 28 | if (json.styles.hasBackgroundColor) { 29 | artboard.hasBackgroundColor = true 30 | artboard.backgroundColor = importColor(json.styles.backgroundColor) 31 | } 32 | parent.object.addLayer(artboard) 33 | current.object = artboard 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/layers/general.js: -------------------------------------------------------------------------------- 1 | import { round } from '../util' 2 | import { getType } from './types' 3 | 4 | function getBound (json) { 5 | return CGRectMake( 6 | parseFloat(json.styles.bounds.origin.x), 7 | parseFloat(json.styles.bounds.origin.y), 8 | parseFloat(json.styles.bounds.size.width), 9 | parseFloat(json.styles.bounds.size.height) 10 | ) 11 | } 12 | 13 | export default class GeneralLayer { 14 | constructor (layer) { 15 | this._layer = layer 16 | } 17 | 18 | id () { 19 | return '' + this._layer.objectID() 20 | } 21 | 22 | shortId () { 23 | return ('' + this._layer.objectID().sha1()).substr(0, 5) 24 | } 25 | 26 | dirName () { 27 | return (this.name() + ' - ' + this.shortId()).replace(new RegExp('/'), ':') 28 | } 29 | 30 | name () { 31 | return '' + this._layer.name() 32 | } 33 | 34 | className () { 35 | return '' + this._layer.className() 36 | } 37 | 38 | stringValue () { 39 | return '' + this._layer.stringValue() 40 | } 41 | 42 | layers () { 43 | var re = [] 44 | if (this._layer.layers) { 45 | re = this._layer.layers() 46 | } 47 | 48 | return re 49 | } 50 | 51 | setLayers (layers) { 52 | this._layers = layers 53 | } 54 | 55 | type () { 56 | return getType(this._layer) 57 | } 58 | 59 | bounds () { 60 | var b = this._layer.frame() 61 | return { 62 | origin: { 63 | x: round(b.x()), 64 | y: round(b.y()) 65 | }, 66 | size: { 67 | width: round(b.width()), 68 | height: round(b.height()) 69 | } 70 | } 71 | } 72 | 73 | styles () { 74 | var re = {} 75 | 76 | if (this._layer.isFlippedHorizontal()) { 77 | re.flippedHorizontal = true 78 | } 79 | 80 | if (this._layer.isFlippedVertical()) { 81 | re.flippedVertical = true 82 | } 83 | 84 | if (this._layer.rotation()) { 85 | re.rotation = this._layer.rotation() 86 | } 87 | 88 | if (this._layer.resizingType()) { 89 | re.resizingType = this._layer.resizingType() 90 | } 91 | 92 | if (this.bounds()) { 93 | re.bounds = this.bounds() 94 | } 95 | 96 | return re 97 | } 98 | 99 | exportJSON () { 100 | const re = { 101 | objectId: this.id(), 102 | type: this.type(), 103 | className: this.className(), 104 | name: this.name(), 105 | styles: this.styles() 106 | } 107 | 108 | if (!this._layer.isVisible()) { 109 | re.hidden = true 110 | } 111 | 112 | if (this._layer.isLocked()) { 113 | re.locked = true 114 | } 115 | 116 | if (this._layer.shouldBreakMaskChain()) { 117 | re.breakMaskChain = true 118 | } 119 | 120 | if (this._layer.hasClickThrough && this._layer.hasClickThrough()) { 121 | re.clickThrough = true 122 | } 123 | 124 | return re 125 | } 126 | 127 | static importBound (json) { 128 | return getBound(json) 129 | } 130 | 131 | static importLayerProps (layer, json) { 132 | layer.objectID = json.objectId 133 | layer.setName(json.name) 134 | if (json.hidden) { 135 | layer.isVisible = false 136 | } 137 | if (json.locked) { 138 | layer.isLocked = true 139 | } 140 | if (json.styles.flippedHorizontal) { 141 | layer.isFlippedHorizontal = true 142 | } 143 | if (json.styles.flippedVertical) { 144 | layer.isFlippedVertical = true 145 | } 146 | if (json.breakMaskChain) { 147 | layer.shouldBreakMaskChain = true 148 | } 149 | if (json.styles.rotation) { 150 | layer.rotation = json.styles.rotation 151 | } 152 | if (json.clickThrough) { 153 | layer.hasClickThrough = true 154 | } 155 | if (json.styles.bounds) { 156 | return MSRect.rectWithRect(getBound(json)) 157 | } 158 | } 159 | 160 | images () { 161 | return [] 162 | } 163 | 164 | setSavedImages (images) { 165 | this.savedImages = images 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /src/layers/groupLayer.js: -------------------------------------------------------------------------------- 1 | import { isNull } from '../util' 2 | import { exportStyle, importStyle } from '../sharedStyle' 3 | import GeneralLayer from './general' 4 | 5 | export default class GroupLayer extends GeneralLayer { 6 | styles () { 7 | return { 8 | ...super.styles(), 9 | ...exportStyle(this._layer.styleGeneric(), this.savedImages) 10 | } 11 | } 12 | 13 | static importJSON (doc, json, parent, current) { 14 | if (!isNull(parent) && !parent.object) { 15 | return 16 | } 17 | 18 | var group = MSLayerGroup.alloc().init() 19 | 20 | group.frame = GeneralLayer.importLayerProps(group, json) 21 | importStyle(group, json.styles) 22 | 23 | parent.object.addLayer(group) 24 | current.object = group 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/layers/imageLayer.js: -------------------------------------------------------------------------------- 1 | import { findImage, imageName, imageId, isNull } from '../util' 2 | import { exportStyle, importStyle } from '../sharedStyle' 3 | import GeneralLayer from './general' 4 | 5 | export default class ImageLayer extends GeneralLayer { 6 | styles () { 7 | const image = findImage(this.savedImages, this._layer.image()) 8 | return { 9 | ...super.styles(), 10 | image: image.sha1, 11 | ...exportStyle(this._layer.styleGeneric(), this.savedImages) 12 | } 13 | } 14 | 15 | images () { 16 | var re = [] 17 | var image = this._layer.image() 18 | re.push({ 19 | name: imageId(image), 20 | image: image.image() 21 | }) 22 | return re 23 | } 24 | 25 | static importJSON (doc, json, parent, current) { 26 | if (!isNull(parent) && !parent.object) { 27 | return 28 | } 29 | 30 | var imagePath = current.path + '/' + imageName({sha1: json.styles.image}) 31 | var image = NSImage.alloc().initWithContentsOfFile(imagePath) 32 | var imageData = MSImageData.alloc().initWithImage_convertColorSpace(image, false) 33 | var bitmap = MSBitmapLayer.alloc().initWithFrame_image(GeneralLayer.importBound(json), imageData) 34 | GeneralLayer.importLayerProps(bitmap, json) 35 | 36 | importStyle(bitmap, json.styles) 37 | 38 | parent.object.addLayer(bitmap) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/layers/pageLayer.js: -------------------------------------------------------------------------------- 1 | import GeneralLayer from './general' 2 | 3 | export default class PageLayer extends GeneralLayer { 4 | bounds () { 5 | return null 6 | } 7 | 8 | static importJSON (doc, json, parent, current) { 9 | const page = doc.addBlankPage() 10 | page.objectID = json.objectId 11 | page.setName(json.name) 12 | current.object = page 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/layers/pathLayer.js: -------------------------------------------------------------------------------- 1 | import GeneralLayer from './general' 2 | import { isNull } from '../util' 3 | 4 | export default class PathLayer extends GeneralLayer { 5 | exportJSON () { 6 | return { 7 | ...super.exportJSON(), 8 | path: '' + this._layer.bezierPath().svgPathAttribute() 9 | } 10 | } 11 | 12 | static importJSON (doc, json, parent, current) { 13 | if (!isNull(parent) && !parent.object) { 14 | return 15 | } 16 | 17 | if (!json.path) { 18 | return 19 | } 20 | 21 | var layer = MSShapePathLayer.alloc().init() 22 | GeneralLayer.importLayerProps(layer, json) 23 | 24 | var isClose = false 25 | var svgAttr = json.path 26 | var regex = new RegExp(' [MLC]?([e0-9,.-]+) Z"$') 27 | if (regex.test(svgAttr)) { 28 | isClose = true 29 | } 30 | var svg = '' 31 | var path = NSBezierPath.bezierPathFromSVGString(svg) 32 | layer.bezierPath = path 33 | 34 | if (isClose) { 35 | layer.closeLastPath(true) 36 | } 37 | 38 | parent.object.addLayer(layer) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/layers/shapeGroupLayer.js: -------------------------------------------------------------------------------- 1 | import { imageId, isNull } from '../util' 2 | import { exportStyle, importStyle } from '../sharedStyle' 3 | import * as types from './types' 4 | import GeneralLayer from './general' 5 | 6 | const objcMap = { 7 | [types.OVAL]: MSOvalShape, 8 | [types.RECTANGLE]: MSRectangleShape, 9 | [types.COMBINED_SHAPE]: MSShapePathLayer, 10 | [types.SHAPE_PATH]: MSShapePathLayer 11 | } 12 | 13 | export default class ShapeGroupLayer extends GeneralLayer { 14 | styles () { 15 | if (this.className() != 'MSShapeGroup') { 16 | return { 17 | ...super.styles(), 18 | ...this.booleanOperation() 19 | } 20 | } 21 | 22 | return { 23 | ...super.styles(), 24 | ...exportStyle(this._layer.styleGeneric(), this.savedImages), 25 | ...(this._layer.hasClippingMask() == 1 && {mask: 'initial'}) 26 | } 27 | } 28 | 29 | booleanOperation () { 30 | var operationStr = '' 31 | var operation = this._layer.booleanOperation() 32 | if (operation < 0) { 33 | return {} 34 | } 35 | 36 | if (operation === 0) { 37 | operationStr = 'union' 38 | } else if (operation === 1) { 39 | operationStr = 'subtract' 40 | } else if (operation === 2) { 41 | operationStr = 'intersect' 42 | } else if (operation === 3) { 43 | operationStr = 'difference' 44 | } 45 | 46 | return { 47 | booleanOperation: operationStr 48 | } 49 | } 50 | 51 | images () { 52 | var re = [] 53 | 54 | if (this.className() != 'MSShapeGroup') { 55 | return re 56 | } 57 | 58 | var fills = this._layer.styleGeneric().fills() 59 | for (var i = 0; i < fills.length; i++) { 60 | if (fills[i].fillType() == 4) { 61 | var image = fills[i].image() 62 | re.push({ 63 | name: imageId(image), 64 | image: image.image() 65 | }) 66 | } 67 | } 68 | 69 | return re 70 | } 71 | 72 | static importJSON (doc, json, parent, current) { 73 | if (!isNull(parent) && !parent.object) { 74 | return 75 | } 76 | 77 | const type = objcMap[json.type] 78 | 79 | var group 80 | if (type !== MSShapePathLayer) { 81 | var shape = type.alloc().init() 82 | shape.frame = MSRect.rectWithRect(GeneralLayer.importBound(json)) 83 | 84 | if (json.styles.borderRadius) { 85 | shape.cornerRadiusFloat = parseFloat(json.styles.borderRadius) 86 | } 87 | 88 | group = MSShapeGroup.shapeWithPath(shape) 89 | importStyle(group, json.styles) 90 | GeneralLayer.importLayerProps(group, json) 91 | } else { 92 | group = MSShapeGroup.alloc().init() 93 | importStyle(group, json.styles) 94 | group.frame = GeneralLayer.importLayerProps(group, json) 95 | } 96 | 97 | if (json.styles.mask) { 98 | group.prepareAsMask() 99 | } 100 | 101 | parent.object.addLayer(group) 102 | current.object = group 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/layers/symbolLayer.js: -------------------------------------------------------------------------------- 1 | import GeneralLayer from './general' 2 | 3 | export default class SymbolLayer extends GeneralLayer { 4 | exportJSON () { 5 | console.dump(this._layer) 6 | return { 7 | ...super.exportJSON(), 8 | symbolId: '' + this._layer.symbolID() 9 | } 10 | } 11 | 12 | static importJSON (doc, json, parent, current) { 13 | var symbol = MSSymbolInstance.alloc().init() 14 | symbol.objectID = json.objectId 15 | symbol.setName(json.name) 16 | symbol.symbolID = json.symbolId 17 | symbol.setRect(GeneralLayer.importBound((json))) 18 | parent.object.addLayer(symbol) 19 | current.object = symbol 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/layers/symbolMasterLayer.js: -------------------------------------------------------------------------------- 1 | import { importColor, exportColor } from '../NS/color' 2 | import GeneralLayer from './general' 3 | 4 | export default class SymbolMasterLayer extends GeneralLayer { 5 | exportJSON () { 6 | return { 7 | ...super.exportJSON(), 8 | symbolId: '' + this._layer.symbolID() 9 | } 10 | } 11 | 12 | styles () { 13 | return { 14 | ...super.styles(), 15 | ...this.background() 16 | } 17 | } 18 | 19 | background () { 20 | var re = {} 21 | 22 | if (this._layer.hasBackgroundColor()) { 23 | re.hasBackgroundColor = true 24 | re.backgroundColor = exportColor(this._layer.backgroundColor()) 25 | } 26 | 27 | return re 28 | } 29 | 30 | static importJSON (doc, json, parent, current) { 31 | var symbol = MSSymbolMaster.alloc().initWithFrame(GeneralLayer.importBound(json)) 32 | symbol.objectID = json.objectId 33 | symbol.setName(json.name) 34 | symbol.symbolID = json.symbolId 35 | if (json.styles.hasBackgroundColor) { 36 | symbol.hasBackgroundColor = true 37 | symbol.backgroundColor = importColor(json.styles.backgroundColor) 38 | } 39 | if (parent) { 40 | parent.object.addLayer(symbol) 41 | } 42 | current.object = symbol 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/layers/textLayer.js: -------------------------------------------------------------------------------- 1 | import { isNull } from '../util' 2 | import { exportTextStyle, importTextStyle } from '../sharedTextStyle' 3 | import { exportStyle, importStyle } from '../sharedStyle' 4 | import GeneralLayer from './general' 5 | 6 | export default class TextLayer extends GeneralLayer { 7 | styles () { 8 | return { 9 | ...super.styles(), 10 | ...this.cssText(), 11 | ...exportStyle(this._layer.styleGeneric(), this.savedImages) 12 | } 13 | } 14 | 15 | cssText () { 16 | var re = {} 17 | if (this.className() == 'MSTextLayer') { 18 | // Content 19 | re.content = this.stringValue() 20 | 21 | const attrs = this._layer.styleAttributes() 22 | 23 | // Shared style 24 | if (this._layer.style().sharedObjectID()) { 25 | re.sharedStyleId = '' + this._layer.style().sharedObjectID() 26 | } 27 | 28 | // Text Behaviour: 0:auto 1:fixed 29 | re.textBehaviour = this._layer.textBehaviour() == 0 ? 'auto' : 'fixed' 30 | 31 | re = { 32 | ...exportTextStyle(attrs), 33 | ...re 34 | } 35 | } 36 | return re 37 | } 38 | 39 | static importJSON (doc, json, parent, current) { 40 | if (isNull(parent) || (!isNull(parent) && !parent.object)) { 41 | return 42 | } 43 | 44 | var text = MSTextLayer.alloc().init() 45 | if (json.styles.content) { 46 | text.stringValue = json.styles.content 47 | } 48 | 49 | if (json.styles.sharedStyleId) { 50 | const sharedStyles = doc.documentData().layerTextStyles().objects() 51 | const sharedStyle = sharedStyles.find(({objectID}) => objectID() == json.styles.sharedStyleId) 52 | if (sharedStyle) { 53 | text.setStyle(sharedStyle.newInstance()) 54 | } 55 | } 56 | 57 | importStyle(text, json.styles) 58 | importTextStyle(text, json.styles) 59 | 60 | if (json.styles.textBehaviour) { 61 | text.textBehaviour = json.styles.textBehaviour == 'auto' ? 0 : 1 62 | } 63 | 64 | text.frame = GeneralLayer.importLayerProps(text, json) 65 | 66 | parent.object.addLayer(text) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/layers/types.js: -------------------------------------------------------------------------------- 1 | import { toArray } from '../util' 2 | 3 | export const PAGE = 'page' 4 | export const ARTBOARD = 'artboard' 5 | export const SLICE = 'slice' 6 | export const SYMBOL_MASTER = 'symbolMaster' 7 | export const SYMBOL = 'symbol' 8 | export const TEXT = 'text' 9 | export const IMAGE = 'image' 10 | export const OVAL = 'oval' 11 | export const RECTANGLE = 'rectangle' 12 | export const SHAPE_PATH = 'shapePath' 13 | export const COMBINED_SHAPE = 'combinedShape' 14 | export const PATH = 'path' 15 | export const GROUP = 'group' 16 | 17 | export function getType (layer) { 18 | switch ('' + layer.className()) { 19 | case 'MSPage': 20 | return PAGE 21 | case 'MSArtboardGroup': 22 | return ARTBOARD 23 | case 'MSLayerGroup': 24 | return GROUP 25 | case 'MSTextLayer': 26 | return TEXT 27 | case 'MSSliceLayer': 28 | return SLICE 29 | case 'MSBitmapLayer': 30 | return IMAGE 31 | case 'MSShapeGroup': 32 | let layers = [] 33 | if (layer.layers) { 34 | layers = toArray(layer.layers()) 35 | } 36 | if (layers.length === 1 && getType(layers[0]) !== PATH) { 37 | return getType(layers[0]) 38 | } else { 39 | return COMBINED_SHAPE 40 | } 41 | case 'MSOvalShape': 42 | return OVAL 43 | case 'MSRectangleShape': 44 | return RECTANGLE 45 | case 'MSShapePathLayer': 46 | return PATH 47 | case 'MSSymbolInstance': 48 | return 'symbol' 49 | case 'MSSymbolMaster': 50 | return 'symbolMaster' 51 | default: 52 | return 'layer' 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/sharedStyle.js: -------------------------------------------------------------------------------- 1 | import { exportFills, importFills } from './NS/fill' 2 | import { exportBorders, importBorders } from './NS/border' 3 | import { exportBlur, importBlur } from './NS/blur' 4 | import { exportShadows, importShadows } from './NS/shadows' 5 | import { exportBlendMode, importBlendMode } from './NS/blendMode' 6 | 7 | export function exportStyle (style, savedImages) { 8 | let re = {} 9 | if (style.fills() && style.fills().length) { 10 | re.backgrounds = exportFills(style.fills(), savedImages) 11 | } 12 | if (style.borders() && style.borders().length) { 13 | re.borders = exportBorders(style.borders()) 14 | } 15 | if (style.shadows() && style.shadows().length) { 16 | re.shadows = exportShadows(style.shadows()) 17 | } 18 | if (style.innerShadows() && style.innerShadows().length) { 19 | if (!re.shadows) { re.shadows = [] } 20 | re.shadows = re.shadows.concat(exportShadows(style.innerShadows(), true)) 21 | } 22 | if (style.blur() && style.blur().isEnabled()) { 23 | re.blur = exportBlur(style.blur()) 24 | } 25 | if (style.contextSettings().opacity() && style.contextSettings().opacity() != 1) { 26 | re.opacity = style.contextSettings().opacity() 27 | } 28 | const mode = style.contextSettings().blendMode() 29 | if (mode > 0) { 30 | re.blendMode = exportBlendMode(mode) 31 | } 32 | return re 33 | } 34 | 35 | export function importStyle (layer, s) { 36 | if (s.backgrounds) { 37 | const fills = importFills(s.backgrounds, '') 38 | fills.forEach(f => layer.style().addStyleFill(f)) 39 | } 40 | if (s.borders) { 41 | const borders = importBorders(s.borders) 42 | borders.forEach(b => layer.style().addStyleBorder(b)) 43 | } 44 | if (s.blur) { 45 | const blur = importBlur(s.blur) 46 | layer.style().blur = blur 47 | } 48 | if (s.shadows) { 49 | const {innerShadows, outerShadows} = importShadows(s.shadows) 50 | layer.style().innerShadows = innerShadows 51 | layer.style().shadows = outerShadows 52 | } 53 | if (s.blendMode) { 54 | layer.style().contextSettings().blendMode = importBlendMode(s.blendMode) 55 | } 56 | if (s.opacity) { 57 | layer.style().contextSettings().opacity = parseFloat(s.opacity) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/sharedTextStyle.js: -------------------------------------------------------------------------------- 1 | import fs from 'sketch-module-fs' 2 | import { exportColor, importColor } from './NS/color' 3 | import { exportFont, importFont } from './NS/font' 4 | import { exportKern, importKern } from './NS/kern' 5 | import { exportSpacing, importSpacing } from './NS/spacing' 6 | import { exportTextDecoration, importTextDecoration } from './NS/textDecoration' 7 | 8 | export function exportTextStyle (attrs) { 9 | let re = {} 10 | if (attrs.NSColor) { 11 | re.color = exportColor(attrs.NSColor) 12 | } 13 | 14 | if (attrs.NSFont) { 15 | re = {...exportFont(attrs.NSFont), ...re} 16 | } 17 | 18 | if (attrs.NSKern) { 19 | re.letterSpacing = exportKern(attrs.NSKern) 20 | } 21 | 22 | if (attrs.NSParagraphStyle) { 23 | re = {...exportSpacing(attrs.NSParagraphStyle), ...re} 24 | } 25 | 26 | return {...exportTextDecoration(attrs), ...re} 27 | } 28 | 29 | export function exportSharedTextStyle (path, layer, index) { 30 | const json = { 31 | objectId: String(layer.objectID()), 32 | name: String(layer.name()), 33 | index, 34 | styles: exportTextStyle(layer.style().textStyle().attributes()) 35 | } 36 | 37 | fs.writeFile(path + '/' + json.name + '.json', JSON.stringify(json, null, ' ')) 38 | } 39 | 40 | export function importTextStyle (text, s) { 41 | if (s.fontFamily) { 42 | text.font = importFont(s) 43 | } 44 | if (s.letterSpacing) { 45 | text.setKerning(importKern(s.letterSpacing)) 46 | } 47 | importSpacing(text, s) 48 | importTextDecoration(text, s) 49 | if (s.color) { 50 | text.textColor = importColor(s.color) 51 | } 52 | } 53 | 54 | export function importSharedTextStyle (doc, json) { 55 | const sharedTextStyles = doc.documentData().layerTextStyles() 56 | 57 | const text = MSTextLayer.alloc().init() 58 | text.stringValue = 'temp text layer for shared style' 59 | 60 | importTextStyle(text, json.styles) 61 | 62 | sharedTextStyles.addSharedStyleWithName_firstInstance(json.name, text.style()) 63 | sharedTextStyles.objects()[sharedTextStyles.objects().length - 1].objectID = json.objectId 64 | } 65 | -------------------------------------------------------------------------------- /src/util.js: -------------------------------------------------------------------------------- 1 | export function isNull (obj) { 2 | return obj == null || Object.prototype.toString.call(obj) == '[object Null]' 3 | } 4 | 5 | export function toArray (t) { 6 | for (var n = t.count(), r = [], e = 0; n > e; e++) { 7 | r.push(t.objectAtIndex(e)) 8 | } 9 | return r 10 | } 11 | 12 | export function imageName (image) { 13 | return image.sha1.substr(0, 7) + '.png' 14 | } 15 | 16 | export function imageId (image) { 17 | return '' + image.sha1().sha1AsString() 18 | } 19 | 20 | function system (path, args) { 21 | if (!args) { 22 | args = [] 23 | } 24 | var task = NSTask.alloc().init() 25 | task.launchPath = path 26 | task.arguments = args 27 | var stdout = NSPipe.pipe() 28 | task.standardOutput = stdout 29 | task.launch() 30 | task.waitUntilExit() 31 | var data = stdout.fileHandleForReading().readDataToEndOfFile() 32 | 33 | return NSString.alloc().initWithData_encoding(data, NSUTF8StringEncoding) 34 | } 35 | 36 | export function round (number, significant = 3) { 37 | return +parseFloat(number).toFixed(significant) 38 | } 39 | 40 | export function shasum (path) { 41 | const out = '' + system('/usr/bin/shasum', [path]) 42 | return out.substr(0, 40) 43 | } 44 | 45 | export function findImage (savedImages, image) { 46 | for (var i = 0; i < savedImages.length; i++) { 47 | if (savedImages[i].name == imageId(image)) { 48 | return savedImages[i] 49 | } 50 | } 51 | return null 52 | } 53 | 54 | export function getCurrentDirectory (context) { 55 | return context.document.fileURL().URLByDeletingLastPathComponent().path() 56 | } 57 | 58 | export function getCurrentFileName (context) { 59 | return context.document.fileURL().lastPathComponent().replace('.sketch', '') 60 | } 61 | --------------------------------------------------------------------------------