diff --git a/.eslintrc.yml b/.eslintrc.yml
index e9dfa10fd..82661d2ea 100644
--- a/.eslintrc.yml
+++ b/.eslintrc.yml
@@ -132,6 +132,14 @@ globals:
MSExportFormat: false
NSThread: false
MSTheme: false
+ MSJSONDataArchiver: false
+ MSJSONDictionaryUnarchiver: false
+ MSArchiveHeader: false
+ NSUTF8StringEncoding: false
+ MSExportRequest: false
+ MSExportManager: false
+ NSFileManager: false
+ NSDataWritingWithoutOverwriting: false
rules:
###########
diff --git a/CHANGELOG.json b/CHANGELOG.json
index 52db6043a..9db4db4bc 100644
--- a/CHANGELOG.json
+++ b/CHANGELOG.json
@@ -10,6 +10,7 @@
"[New] Add some methods to store a session variable",
"[Improved] Allow using setting methods even from the Run Script panel",
"[New] Add method to get the theme of Sketch",
+ "[New] `export` can now export to the JSON file format"
"[New] Add `background` property on Artboard",
"[New] Add `transform` property on Layer"
],
diff --git a/Source/dom/__tests__/export.test.js b/Source/dom/__tests__/export.test.js
new file mode 100644
index 000000000..5df6f0820
--- /dev/null
+++ b/Source/dom/__tests__/export.test.js
@@ -0,0 +1,71 @@
+/* globals expect, test */
+
+import { exportObject, objectFromJSON } from '../export'
+import { Shape } from '../layers/Shape'
+
+test('should return exported json data', () => {
+ const object = new Shape()
+ const archive = exportObject(object, {
+ formats: 'json',
+ output: false,
+ })
+ expect(archive.do_objectID).toEqual(String(object.id))
+ expect(archive._class).toEqual('shapeGroup')
+})
+
+test('should return array of exported json data', () => {
+ const objects = [new Shape(), new Shape()]
+ const archive = exportObject(objects, {
+ formats: 'json',
+ output: false,
+ })
+ expect(archive.length).toBe(2)
+ expect(archive[0].do_objectID).toEqual(String(objects[0].id))
+ expect(archive[1].do_objectID).toEqual(String(objects[1].id))
+})
+
+test('should restore object from json data', () => {
+ const object = new Shape()
+ const archive = exportObject(object, {
+ formats: ['json'],
+ output: false,
+ })
+ const restored = objectFromJSON(archive)
+ expect(restored.id).toEqual(String(object.id))
+})
+
+test('Should fail with no object provided', () => {
+ try {
+ exportObject([], {
+ output: false,
+ })
+ } catch (err) {
+ expect(err.message).toMatch('No objects provided to export')
+ }
+})
+
+test('Should fail to return image data', () => {
+ try {
+ const object = new Shape()
+ exportObject(object, {
+ formats: 'png',
+ output: false,
+ })
+ } catch (err) {
+ expect(err.message).toMatch(
+ 'Return output is only support for the json format'
+ )
+ }
+})
+
+test('should fail with to return with mulitple formats', () => {
+ try {
+ const object = new Shape()
+ exportObject(object, {
+ formats: ['png', 'json'],
+ output: false,
+ })
+ } catch (err) {
+ expect(err.message).toMatch('Can only return 1 format with no output type')
+ }
+})
diff --git a/Source/dom/export.js b/Source/dom/export.js
index 4630a9ea6..94358635a 100644
--- a/Source/dom/export.js
+++ b/Source/dom/export.js
@@ -1,5 +1,5 @@
import { isWrappedObject } from './utils'
-import { Types } from './enums'
+import { wrapNativeObject } from './wrapNativeObject'
export const DEFAULT_EXPORT_OPTIONS = {
compact: false,
@@ -14,6 +14,83 @@ export const DEFAULT_EXPORT_OPTIONS = {
output: '~/Documents/Sketch Exports',
}
+function getJSONData(nativeObject) {
+ const archiver = MSJSONDataArchiver.new()
+ archiver.archiveObjectIDs = true
+ const aPtr = MOPointer.alloc().init()
+ const obj = nativeObject.immutableModelObject
+ ? nativeObject.immutableModelObject()
+ : nativeObject
+ archiver.archivedDataWithRootObject_error(obj, aPtr)
+ if (aPtr.value()) {
+ throw Error(`Couldn’t create the JSON string: ${aPtr.value()}`)
+ }
+ return archiver.archivedData()
+}
+
+function getJSONString(nativeObject) {
+ const data = getJSONData(nativeObject)
+ return String(
+ NSString.alloc().initWithData_encoding(data, NSUTF8StringEncoding)
+ )
+}
+
+function exportToJSONFile(nativeObjects, options) {
+ const fm = NSFileManager.defaultManager()
+ const directory = NSString.stringWithString(
+ options.output
+ ).stringByExpandingTildeInPath()
+ const comps = String(directory).split('/')
+ fm.createDirectoryAtPath_withIntermediateDirectories_attributes_error(
+ directory,
+ true,
+ null,
+ null
+ )
+
+ nativeObjects.forEach(o => {
+ const name =
+ options['use-id-for-name'] === true || !o.name ? o.objectID() : o.name()
+ const pathComps = comps.slice()
+ pathComps.push(`${name}.json`)
+ const url = NSURL.fileURLWithPath(pathComps.join('/'))
+ const data = getJSONData(o)
+ const writeOptions = options.overwriting
+ ? 0
+ : NSDataWritingWithoutOverwriting
+ const ptr = MOPointer.new()
+ data.writeToURL_options_error(url, writeOptions, ptr)
+ if (ptr.value()) {
+ throw new Error(`Error writing json file ${ptr.value()}`)
+ }
+ })
+}
+
+function exportToImageFile(nativeObjects, options) {
+ // we need to class the objects by types as we need to do different things depending on it
+ const pages = []
+ const layers = []
+ nativeObjects.forEach(o => {
+ if (o.isKindOfClass(MSPage)) {
+ pages.push(o)
+ } else {
+ layers.push(o)
+ }
+ })
+
+ const exporter = MSSelfContainedHighLevelExporter.alloc().initWithOptions(
+ options
+ )
+
+ // export the pages
+ pages.forEach(exporter.exportPage)
+
+ // export the layers
+ if (layers.length) {
+ exporter.exportLayers(layers)
+ }
+}
+
/**
* Export an object, using the options supplied.
*
@@ -26,11 +103,11 @@ export const DEFAULT_EXPORT_OPTIONS = {
* ### General Options
*
* - use-id-for-name : normally the exported files are given the same names as the layers they represent, but if this options is true, then the layer ids are used instead; defaults to false.
- * - output : this is the path of the folder where all exported files are placed; defaults to "~/Documents/Sketch Exports"
+ * - output : this is the path of the folder where all exported files are placed; defaults to "~/Documents/Sketch Exports". If falsey the data is returned immediately (only supported for json).
* - overwriting : if true, the exporter will overwrite any existing files with new ones; defaults to false.
* - trimmed: if true, any transparent space around the exported image will be trimmed; defaults to false.
* - scales: this should be a list of numbers; it will determine the sizes at which the layers are exported; defaults to "1"
- * - formats: this should be a list of one or more of "png", "jpg", "svg", and "pdf"; defaults to "png" (see discussion below)
+ * - formats: this should be a list of one or more of "png", "jpg", "svg", "json", and "pdf"; defaults to "png" (see discussion below)
*
* ### SVG options
* - compact : if exporting as SVG, this option makes the output more compact; defaults to false.
@@ -46,58 +123,92 @@ export const DEFAULT_EXPORT_OPTIONS = {
*
*
* @param {dictionary} options Options indicating which sizes and formats to use, etc.
+ *
+ * @returns If an output path is not set, the data is returned
*/
export function exportObject(object, options) {
- const merged = { ...DEFAULT_EXPORT_OPTIONS, ...options }
- const exporter = MSSelfContainedHighLevelExporter.alloc().initWithOptions(
- merged
- )
+ // Validate the provided objects
+ const objectsToExport = (Array.isArray(object) ? object : [object])
+ .map(o => (isWrappedObject(o) ? o.sketchObject : o))
+ .filter(o => o)
- function exportNativeLayers(layers) {
- exporter.exportLayers(layers)
+ if (!objectsToExport.length) {
+ throw new Error('No objects provided to export')
}
- function exportNativePage(page) {
- exporter.exportPage(page)
+ // Validate export formats
+ let formats = (options || {}).formats || []
+ if (typeof formats === 'string') {
+ formats = formats.split(',')
}
- if (Array.isArray(object)) {
- const isArrayOfPages = isWrappedObject(object[0])
- ? object[0].type === Types.Page
- : String(object[0].class()) === 'MSPage'
-
- if (isArrayOfPages) {
- // support an array of pages
- object.forEach(o => {
- if (isWrappedObject(o)) {
- exportNativePage(o.sketchObject)
- } else {
- exportNativePage(o)
- }
- })
- } else {
- // support an array of layers
- exportNativeLayers(
- object.map(o => {
- if (isWrappedObject(o)) {
- return o.sketchObject
- }
- return o
- })
- )
- }
- } else if (isWrappedObject(object)) {
- // support a wrapped object
- if (object.type === Types.Page) {
- exportNativePage(object.sketchObject)
- } else {
- exportNativeLayers([object.sketchObject])
+ formats = formats.map(format => format.trim())
+
+ // if we don't have any format, we default to png
+ if (formats.length === 0) {
+ formats.push('png')
+ }
+
+ const shouldExportToJSON = formats.indexOf('json') !== -1
+ const exportImagesFormat = formats.filter(format => format !== 'json')
+
+ const optionsWithDefaults = {
+ ...DEFAULT_EXPORT_OPTIONS,
+ ...options,
+ ...{ formats: exportImagesFormat.join(',') },
+ }
+
+ // Return data if no output directory specified
+ if (!optionsWithDefaults.output) {
+ if (formats.length > 1) {
+ throw new Error('Can only return 1 format with no output type')
}
- } else if (String(object.class()) === 'MSPage') {
- // support a native page
- exportNativePage(object)
- } else {
- // support a native layer
- exportNativeLayers([object])
+ const format = formats[0]
+ const exported = objectsToExport.map(nativeObject => {
+ if (format === 'json') {
+ const str = getJSONString(nativeObject)
+ return JSON.parse(str)
+ }
+ // Insert code for returning image data here...
+ throw new Error('Return output is only support for the json format')
+ })
+ // Return the same format that was provided
+ return Array.isArray(object) ? exported : exported[0]
+ }
+
+ if (shouldExportToJSON) {
+ exportToJSONFile(objectsToExport, optionsWithDefaults)
+ }
+
+ if (exportImagesFormat.length) {
+ exportToImageFile(objectsToExport, optionsWithDefaults)
+ }
+ return true
+}
+
+/**
+ * Create an object from the exported Sketch JSON.
+ *
+ * @param {dictionary} sketchJSON The exported Sketch JSON data
+ * @param {number} version The file version that the Sketch JSON
+ * was exported from. Defaults to the current version
+ * @returns {WrappedObject} A javascript object (subclass of WrappedObject),
+ * which represents the restored Sketch object.
+ */
+export function objectFromJSON(sketchJSON, version) {
+ const v = version || MSArchiveHeader.metadataForNewHeader().version
+ const ptr = MOPointer.new()
+ let object = MSJSONDictionaryUnarchiver.unarchiveObjectFromDictionary_asVersion_corruptionDetected_error(
+ sketchJSON,
+ v,
+ null,
+ ptr
+ )
+ if (ptr.value()) {
+ throw new Error(`Failed to create object from sketch JSON: ${ptr.value()}`)
+ }
+ if (object.newMutableCounterpart) {
+ object = object.newMutableCounterpart()
}
+ return wrapNativeObject(object)
}
diff --git a/Source/dom/index.js b/Source/dom/index.js
index 44cf60342..af862acab 100755
--- a/Source/dom/index.js
+++ b/Source/dom/index.js
@@ -1,7 +1,10 @@
import { AnimationType, BackTarget } from './models/Flow'
import './models/DataOverride'
-export { exportObject as export } from './export'
+export {
+ exportObject as export,
+ objectFromJSON as fromSketchJSON,
+} from './export'
export { Document, getDocuments, getSelectedDocument } from './models/Document'
export { Library, getLibraries } from './models/Library'
diff --git a/docs/api/export.md b/docs/api/export.md
index 73dd7f738..068efe653 100644
--- a/docs/api/export.md
+++ b/docs/api/export.md
@@ -24,21 +24,26 @@ sketch.export(page, options)
sketch.export(document.pages)
```
+```javascript
+const options = { formats: 'json', output: false }
+const sketchJSON = sketch.export(layer, options)
+```
+
Export an object, using the options supplied.
-| Parameters | |
-| ----------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
-| objectToExport[Layer](#layer) / [Layer](#layer)[] / [Page](#page) / [Page](#page)[] | The object to export. |
-| optionsobject | Options indicating which sizes and formats to use, etc.. |
-| options.outputstring | this is the path of the folder where all exported files are placed (defaults to `"~/Documents/Sketch Exports"`). |
-| options.formatsstring | Comma separated list of formats to export to (`png`, `jpg`, `svg` or `pdf`) (default to `"png"`). |
-| options.scalesstring | Comma separated list of scales which determine the sizes at which the layers are exported (defaults to `"1"`). |
-| options['use-id-for-name']boolean | Name exported images using their id rather than their name (defaults to `false`). |
-| options['group-contents-only']boolean | Export only layers that are contained within the group (default to `false`). |
-| options.overwritingboolean | Overwrite existing files (if any) with newly generated ones (defaults to `false`). |
-| options.trimmedboolean | Trim any transparent space around the exported image (defaults to `false`). |
-| options['save-for-web']boolean | If exporting a PNG, remove metadata such as the colour profile from the exported file (defaults to `false`). |
-| options.compactboolean | If exporting a SVG, make the output more compact (defaults to `false`). |
-| options['include-namespaces']boolean | If exporting a SVG, include extra attributes (defaults to `false`). |
-| options.progressiveboolean | If exporting a JPG, export a progressive JPEG (only used when exporting to `jpeg`) (defaults to `false`). |
-| options.compressionnumber | If exporting a JPG, the compression level to use fo `jpeg` (with `0` being the completely compressed, `1.0` no compression) (defaults to `1.0`). |
+| Parameters | |
+|-------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| objectToExport[Layer](#layer) / [Layer](#layer)[] / [Page](#page) / [Page](#page)[] | The object to export. |
+| optionsobject | Options indicating which sizes and formats to use, etc.. |
+| options.outputstring | this is the path of the folder where all exported files are placed (defaults to `"~/Documents/Sketch Exports"`). If falsey, the data for the first object/format is returned immmediately (currently only supported for json). |
+| options.formatsstring | Comma separated list of formats to export to (`png`, `jpg`, `svg`, `json` or `pdf`) (default to `"png"`). |
+| options.scalesstring | Comma separated list of scales which determine the sizes at which the layers are exported (defaults to `"1"`). |
+| options['use-id-for-name']boolean | Name exported images using their id rather than their name (defaults to `false`). |
+| options['group-contents-only']boolean | Export only layers that are contained within the group (default to `false`). |
+| options.overwritingboolean | Overwrite existing files (if any) with newly generated ones (defaults to `false`). |
+| options.trimmedboolean | Trim any transparent space around the exported image (defaults to `false`). |
+| options['save-for-web']boolean | If exporting a PNG, remove metadata such as the colour profile from the exported file (defaults to `false`). |
+| options.compactboolean | If exporting a SVG, make the output more compact (defaults to `false`). |
+| options['include-namespaces']boolean | If exporting a SVG, include extra attributes (defaults to `false`). |
+| options.progressiveboolean | If exporting a JPG, export a progressive JPEG (only used when exporting to `jpeg`) (defaults to `false`). |
+| options.compressionnumber | If exporting a JPG, the compression level to use fo `jpeg` (with `0` being the completely compressed, `1.0` no compression) (defaults to `1.0`). |