├── .npmignore ├── sketch_github.gif ├── elm-package.json ├── example ├── Main.elm ├── elm-package.json └── SketchExample.elm ├── tests └── elm-package.json ├── src ├── Assets.elm ├── Render │ ├── Runtime.elm │ ├── Css.elm │ └── File.elm ├── Runner.elm └── Layer.elm ├── package.json ├── .gitignore ├── LICENSE ├── README_we.md ├── README.md ├── CODE_OF_CONDUCT.md └── index.js /.npmignore: -------------------------------------------------------------------------------- 1 | sketch_github.gif 2 | node_modules 3 | elm-stuff 4 | example 5 | gen 6 | tests 7 | src -------------------------------------------------------------------------------- /sketch_github.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eeue56/elm-sketch-importer/HEAD/sketch_github.gif -------------------------------------------------------------------------------- /elm-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0", 3 | "summary": "helpful summary of your project, less than 80 characters", 4 | "repository": "https://github.com/user/project.git", 5 | "license": "BSD3", 6 | "source-directories": [ 7 | "src" 8 | ], 9 | "exposed-modules": [], 10 | "dependencies": { 11 | "elm-lang/core": "5.1.1 <= v < 6.0.0", 12 | "elm-lang/html": "2.0.0 <= v < 3.0.0", 13 | "truqu/elm-base64": "1.0.5 <= v < 2.0.0" 14 | }, 15 | "elm-version": "0.18.0 <= v < 0.19.0" 16 | } 17 | -------------------------------------------------------------------------------- /example/Main.elm: -------------------------------------------------------------------------------- 1 | module Main exposing (..) 2 | 3 | import Html 4 | import Json.Decode as Json 5 | import Layer 6 | import Render.Runtime 7 | import SketchExample 8 | 9 | 10 | decodeLayers : Json.Decoder (List Layer.Layer) 11 | decodeLayers = 12 | Json.field "layers" (Json.list Layer.decodeLayer) 13 | 14 | 15 | main : Html.Html msg 16 | main = 17 | case Json.decodeString decodeLayers SketchExample.json of 18 | Err r -> 19 | Html.text r 20 | 21 | Ok v -> 22 | List.map Render.Runtime.toHtml v 23 | |> Html.div [] 24 | -------------------------------------------------------------------------------- /example/elm-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0", 3 | "summary": "helpful summary of your project, less than 80 characters", 4 | "repository": "https://github.com/user/project.git", 5 | "license": "BSD3", 6 | "source-directories": [ 7 | ".", 8 | "../src" 9 | ], 10 | "exposed-modules": [], 11 | "dependencies": { 12 | "elm-lang/core": "5.1.1 <= v < 6.0.0", 13 | "elm-lang/html": "2.0.0 <= v < 3.0.0", 14 | "truqu/elm-base64": "1.0.5 <= v < 2.0.0" 15 | }, 16 | "elm-version": "0.18.0 <= v < 0.19.0" 17 | } 18 | -------------------------------------------------------------------------------- /tests/elm-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0", 3 | "summary": "Test Suites", 4 | "repository": "https://github.com/user/project.git", 5 | "license": "BSD3", 6 | "source-directories": [ 7 | "../src", 8 | "." 9 | ], 10 | "exposed-modules": [], 11 | "dependencies": { 12 | "eeue56/elm-html-test": "4.1.0 <= v < 5.0.0", 13 | "elm-community/elm-test": "4.0.0 <= v < 5.0.0", 14 | "elm-lang/core": "5.1.1 <= v < 6.0.0", 15 | "elm-lang/html": "2.0.0 <= v < 3.0.0", 16 | "truqu/elm-base64": "1.0.5 <= v < 2.0.0" 17 | }, 18 | "elm-version": "0.18.0 <= v < 0.19.0" 19 | } 20 | -------------------------------------------------------------------------------- /src/Assets.elm: -------------------------------------------------------------------------------- 1 | module Assets exposing (..) 2 | 3 | {-| Used to figure out the file name we should put in generated code 4 | identifySuffix ["hello.png"] "hello" 5 | --> "hello.png" 6 | 7 | identifySuffix [ "a.png", "ab.jpg"] "ab" 8 | --> "ab.jpg" 9 | 10 | identifySuffix [] "a" 11 | --> "a" 12 | -} 13 | 14 | 15 | identifySuffix : List String -> String -> String 16 | identifySuffix knownImages imagePrefix = 17 | knownImages 18 | |> List.filterMap 19 | (\x -> 20 | case String.split "." x of 21 | [] -> 22 | Nothing 23 | 24 | y :: ys -> 25 | if y == imagePrefix then 26 | Just x 27 | else 28 | Nothing 29 | ) 30 | |> List.head 31 | |> Maybe.withDefault imagePrefix 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "elm-sketch-importer", 3 | "version": "0.0.3", 4 | "description": "An Elm importer from Sketch", 5 | "main": "index.js", 6 | "bin": { 7 | "elm-sketch-importer": "index.js" 8 | }, 9 | "files": "elm.js", 10 | "scripts": { 11 | "build": "./build.sh", 12 | "test": "echo \"Error: no test specified\" && exit 1" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/eeue56/elm-sketch-importer.git" 17 | }, 18 | "keywords": [ 19 | "elm", 20 | "sketch", 21 | "importer", 22 | "exporter", 23 | "static", 24 | "design" 25 | ], 26 | "author": "Noah Hall (eeue56)", 27 | "license": "ISC", 28 | "bugs": { 29 | "url": "https://github.com/eeue56/elm-sketch-importer/issues" 30 | }, 31 | "homepage": "https://github.com/eeue56/elm-sketch-importer#readme", 32 | "dependencies": { 33 | "decompress": "^4.2.0", 34 | "progress": "^2.0.0", 35 | "yargs": "^8.0.2" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | elm-stuff 2 | elm.js 3 | .DS_Store 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 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 | 24 | # nyc test coverage 25 | .nyc_output 26 | 27 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 28 | .grunt 29 | 30 | # Bower dependency directory (https://bower.io/) 31 | bower_components 32 | 33 | # node-waf configuration 34 | .lock-wscript 35 | 36 | # Compiled binary addons (http://nodejs.org/api/addons.html) 37 | build/Release 38 | 39 | # Dependency directories 40 | node_modules/ 41 | jspm_packages/ 42 | 43 | # Typescript v1 declaration files 44 | typings/ 45 | 46 | # Optional npm cache directory 47 | .npm 48 | 49 | # Optional eslint cache 50 | .eslintcache 51 | 52 | # Optional REPL history 53 | .node_repl_history 54 | 55 | # Output of 'npm pack' 56 | *.tgz 57 | 58 | # Yarn Integrity file 59 | .yarn-integrity 60 | 61 | # dotenv environment variables file 62 | .env 63 | 64 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2017, Noah 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /src/Render/Runtime.elm: -------------------------------------------------------------------------------- 1 | module Render.Runtime exposing (toHtml) 2 | 3 | import Html 4 | import Html.Attributes as HA 5 | import Layer exposing (..) 6 | import Render.Css exposing (..) 7 | 8 | 9 | layerPropsToHtml : LayerProps -> Html.Html msg 10 | layerPropsToHtml props = 11 | frameToCss props.frame 12 | ++ styleToCss props.style 13 | |> (\x -> Html.div [ HA.style x ] []) 14 | 15 | 16 | textToHtml : LayerProps -> Text -> Html.Html msg 17 | textToHtml layerProps text = 18 | frameToCss layerProps.frame 19 | ++ styleToCss layerProps.style 20 | ++ horizontalFlip text.isFlippedHorizontal 21 | ++ verticalFlip text.isFlippedVertical 22 | ++ [ ( "z-index", "1" ) ] 23 | |> (\x -> Html.div [ HA.style x ] [ Html.text text.attributedString ]) 24 | 25 | 26 | bitmapToHtml : LayerProps -> ImageProps -> Html.Html msg 27 | bitmapToHtml layerProps image = 28 | frameToCss layerProps.frame 29 | ++ styleToCss layerProps.style 30 | ++ [ ( "z-index", "1" ) ] 31 | |> (\x -> Html.div [ HA.style x ] [ Html.img [ HA.src image.src, HA.alt image.name ] [] ]) 32 | 33 | 34 | {-| Turns a sketch layer into a Html value at runtime 35 | 36 | -} 37 | toHtml : Layer -> Html.Html msg 38 | toHtml layer = 39 | case layer of 40 | Unknown stuff -> 41 | Html.text "Don't know. Please open an issue on Github!" 42 | 43 | Slice -> 44 | Html.text "Unsupported slice. Please open an issue on Github!" 45 | 46 | ShapeGroupLayer layerProps shapeGroup -> 47 | layerPropsToHtml layerProps 48 | 49 | TextLayer layerProps text -> 50 | textToHtml layerProps text 51 | 52 | BitmapLayer layerProps image -> 53 | bitmapToHtml layerProps image 54 | 55 | GroupLayer layers -> 56 | List.map toHtml layers 57 | |> Html.div [] 58 | -------------------------------------------------------------------------------- /src/Runner.elm: -------------------------------------------------------------------------------- 1 | port module Runner exposing (main) 2 | 3 | import Platform 4 | import Json.Decode as Json 5 | import Layer exposing (decodeLayer) 6 | import Render.File exposing (toElmHtml) 7 | 8 | 9 | port parse : (String -> msg) -> Sub msg 10 | 11 | 12 | port knownImages : (List String -> msg) -> Sub msg 13 | 14 | 15 | port respond : String -> Cmd msg 16 | 17 | 18 | type Msg 19 | = Parse String 20 | | KnownImages (List String) 21 | 22 | 23 | type alias Model = 24 | { knownImages : List String } 25 | 26 | 27 | decodeLayers : Json.Decoder (List Layer.Layer) 28 | decodeLayers = 29 | Json.field "layers" (Json.list Layer.decodeLayer) 30 | 31 | 32 | wrapContent : List String -> String 33 | wrapContent divs = 34 | """import Html 35 | import Html.Attributes 36 | 37 | main : Html.Html msg 38 | main = 39 | Html.div 40 | [] 41 | [ 42 | """ ++ String.join "\n\n ," divs ++ "]" 43 | 44 | 45 | update : Msg -> Model -> ( Model, Cmd Msg ) 46 | update msg model = 47 | case msg of 48 | Parse value -> 49 | case Json.decodeString decodeLayers value of 50 | Err r -> 51 | "unable to parse" 52 | |> respond 53 | |> (\x -> ( model, x )) 54 | 55 | Ok v -> 56 | List.map (toElmHtml model.knownImages) v 57 | |> wrapContent 58 | |> respond 59 | |> (\x -> ( model, x )) 60 | 61 | KnownImages images -> 62 | ( { model | knownImages = images }, Cmd.none ) 63 | 64 | 65 | subscriptions : Model -> Sub Msg 66 | subscriptions _ = 67 | Sub.batch 68 | [ parse Parse 69 | , knownImages KnownImages 70 | ] 71 | 72 | 73 | main : Program Never Model Msg 74 | main = 75 | Platform.program 76 | { init = ( { knownImages = [] }, Cmd.none ) 77 | , update = update 78 | , subscriptions = subscriptions 79 | } 80 | -------------------------------------------------------------------------------- /src/Render/Css.elm: -------------------------------------------------------------------------------- 1 | module Render.Css exposing (..) 2 | 3 | import Layer exposing (..) 4 | 5 | 6 | frameToCss : Frame -> List ( String, String ) 7 | frameToCss frame = 8 | [ ( "position", "absolute" ) 9 | , ( "left", toString frame.x ++ "px" ) 10 | , ( "top", toString frame.y ++ "px" ) 11 | , ( "width", toString frame.width ++ "px" ) 12 | , ( "height", toString frame.height ++ "px" ) 13 | ] 14 | 15 | 16 | shapeStylingToCss : ShapeStyling -> List ( String, String ) 17 | shapeStylingToCss style = 18 | case style.fills of 19 | fill :: _ -> 20 | [ ( "background-color" 21 | , "rgba(" 22 | ++ toString (fill.color.red * 255 |> floor) 23 | ++ "," 24 | ++ toString (fill.color.green * 255 |> floor) 25 | ++ "," 26 | ++ toString (fill.color.blue * 255 |> floor) 27 | ++ "," 28 | ++ toString fill.color.alpha 29 | ++ ")" 30 | ) 31 | ] 32 | 33 | _ -> 34 | [] 35 | 36 | 37 | styleToCss : Style -> List ( String, String ) 38 | styleToCss style = 39 | case style of 40 | ShapeStyle shapeStyling -> 41 | shapeStylingToCss shapeStyling 42 | 43 | TextStyle textStyling -> 44 | [] 45 | 46 | 47 | verticalFlip : Bool -> List ( String, String ) 48 | verticalFlip flipped = 49 | if flipped then 50 | [ ( "-moz-transform" 51 | , "scale(1, -1)" 52 | ) 53 | , ( "-webkit-transform" 54 | , "scale(1, -1)" 55 | ) 56 | , ( "-o-transform" 57 | , "scale(1, -1)" 58 | ) 59 | , ( "-ms-transform" 60 | , "scale(1, -1)" 61 | ) 62 | , ( "transform" 63 | , "scale(1, -1)" 64 | ) 65 | ] 66 | else 67 | [] 68 | 69 | 70 | horizontalFlip : Bool -> List ( String, String ) 71 | horizontalFlip flipped = 72 | if flipped then 73 | [ ( "-moz-transform" 74 | , "scale(-1, 1)" 75 | ) 76 | , ( "-webkit-transform" 77 | , "scale(-1, 1)" 78 | ) 79 | , ( "-o-transform" 80 | , "scale(-1, 1)" 81 | ) 82 | , ( "-ms-transform" 83 | , "scale(-1, 1)" 84 | ) 85 | , ( "transform" 86 | , "scale(-1, 1)" 87 | ) 88 | ] 89 | else 90 | [] 91 | -------------------------------------------------------------------------------- /src/Render/File.elm: -------------------------------------------------------------------------------- 1 | module Render.File exposing (toElmHtml) 2 | 3 | import Layer exposing (..) 4 | import Assets exposing (..) 5 | import Render.Css exposing (..) 6 | 7 | 8 | layerPropsToElmHtml : LayerProps -> String 9 | layerPropsToElmHtml props = 10 | frameToCss props.frame 11 | ++ styleToCss props.style 12 | |> List.map (\( x, y ) -> "(\"" ++ x ++ "\", \"" ++ y ++ "\")") 13 | |> String.join "\n , " 14 | |> (\x -> "Html.div [ Html.Attributes.style [" ++ x ++ "] ] []") 15 | 16 | 17 | textToElmHtml : LayerProps -> Text -> String 18 | textToElmHtml layerProps text = 19 | frameToCss layerProps.frame 20 | ++ styleToCss layerProps.style 21 | ++ horizontalFlip text.isFlippedHorizontal 22 | ++ verticalFlip text.isFlippedVertical 23 | ++ [ ( "z-index", "1" ) ] 24 | |> List.map (\( x, y ) -> "(\"" ++ x ++ "\", \"" ++ y ++ "\")") 25 | |> String.join "\n , " 26 | |> (\x -> "Html.div [ Html.Attributes.style [" ++ x ++ "] ] [ Html.text \"" ++ text.name ++ "\" ]") 27 | 28 | 29 | bitmapToElmHtml : List String -> LayerProps -> ImageProps -> String 30 | bitmapToElmHtml knownImages layerProps image = 31 | frameToCss layerProps.frame 32 | ++ styleToCss layerProps.style 33 | ++ [ ( "z-index", "1" ) ] 34 | |> List.map (\( x, y ) -> "(\"" ++ x ++ "\", \"" ++ y ++ "\")") 35 | |> String.join "\n , " 36 | |> (\x -> 37 | "Html.div [ Html.Attributes.style [" 38 | ++ "] ]" 39 | ++ " [ Html.img [ Html.Attributes.src \"" 40 | ++ identifySuffix knownImages image.src 41 | ++ "\", Html.Attributes.style [" 42 | ++ x 43 | ++ "] ] [] ]" 44 | ) 45 | 46 | 47 | toElmHtml : List String -> Layer -> String 48 | toElmHtml knownImages layer = 49 | case layer of 50 | Unknown stuff -> 51 | "Html.text \"\" -- I was unable to figure out what this layer was" ++ toString stuff 52 | 53 | Slice -> 54 | "Html.text \"\" -- Unsupported slice!" 55 | 56 | ShapeGroupLayer layerProps shapeGroup -> 57 | layerPropsToElmHtml layerProps 58 | 59 | TextLayer layerProps text -> 60 | textToElmHtml layerProps text 61 | 62 | BitmapLayer layerProps image -> 63 | bitmapToElmHtml knownImages layerProps image 64 | 65 | GroupLayer layers -> 66 | List.map (toElmHtml knownImages) layers 67 | |> String.join "\n , " 68 | |> (\str -> "Html.div [] [" ++ str ++ "]") 69 | -------------------------------------------------------------------------------- /README_we.md: -------------------------------------------------------------------------------- 1 | # elm-sketch-importer 2 | 3 | Cymryd Sketch ffeil, ac trwy e mewn i Elm. Mae hwn daionus pryd mae gen chi eich dylunio mewn Sketch ac esial gael nhw mewn Elm yn gyflym. Nagw e'n cymryd lle o eich gwaith arferol. 4 | 5 | Mae gwaith ar hyn yn mynd lan. Croseo fawr i pob PR! 6 | 7 | ![](./sketch_github.gif) 8 | 9 | ## Gosod 10 | 11 | Rydym ni defnydd elm-format i neud yr Elm edrych yn pert, felly mae gwell gan chi os bydd e ganddo. 12 | 13 | ``` 14 | npm install -g elm-format 15 | ``` 16 | 17 | Mae elm-sketch-importer yn hawdd iawn i gosod. 18 | 19 | ``` 20 | npm install -g elm-sketch-importer 21 | ``` 22 | 23 | ## Defyndd 24 | 25 | 26 | ``` 27 | Defyndd: [sketchffiel] 28 | 29 | Options: 30 | -h, --help Dangos help [boolean] 31 | -o, --output Rhowch yr lle bod chi'n angen yr Elm fynd 32 | [cyfredol: "generated"] 33 | --ef, --elmformat Rhowch yr lle bod eich elm-format byw 34 | [cyfredol: "elm-format"] 35 | 36 | Dangosiad: 37 | elm-sketch-importer ~/Documents/example.sketch 38 | ``` 39 | 40 | 41 | ## Cymorth 42 | 43 | ## Layers 44 | 45 | ### Shapes ac rects 46 | 47 | | Feature | Supported? | 48 | |---------|-------------| 49 | | Rectangles | :white_check_mark: | 50 | | Layer positions | :white_check_mark: | 51 | | Layer sizes | :white_check_mark: | 52 | | Multiple layers | :white_check_mark: | 53 | | Fills | :white_check_mark: | 54 | | Border | :warning: | 55 | | Border color | :warning: | 56 | | Colored fills | :white_check_mark: | 57 | | Other shapes | :warning: | 58 | | Groups | :warning: | 59 | | Slices | :warning: | 60 | | Images | :white_check_mark: | 61 | 62 | 63 | ### Text 64 | 65 | Nac ydy testun hir yn mynd mas i Elm. Mae hwn lawr i yr faith bod BPLists yn anodd iawn i parsio mewn Elm. Bydd e'n dod cyn bod hir. 66 | 67 | 68 | | Feature | Supported? | 69 | |---------|------------| 70 | | Short text | :white_check_mark: | 71 | | Long text | :warning: | 72 | | Horizional/vertical flips | :white_check_mark: | 73 | | Position | :white_check_mark: | 74 | | Size | :warning: | 75 | | Color | :warning: | 76 | | Font | :warning: | 77 | 78 | 79 | ## Amserlen 80 | 81 | Mae amserlen hwnw yn fod i bod blaenoriaeth. Nac ydy popeth arno yma, ac mae yr pethiau sy'n yma galli newid. Ond trwy gweitho i gilydd mae popeth galli dod yn gynt :) 82 | 83 | # Cyn 1.0 84 | 85 | - Relative layout instead of fixed pixels 86 | - Full support for importing Sketch files 87 | - Make sure that no features remain unsupported 88 | 89 | # Ti ol 1.0 90 | 91 | - Export views to Sketch 92 | 93 | # Misc 94 | 95 | - Generating elm-css or style-element based views 96 | 97 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # elm-sketch-importer 2 | 3 | Takes a Sketch file, and generates Elm out of it. This is particularly useful if you want to quickly export some part of a design into Elm. 4 | 5 | This is a work in progress. PRs are welcome! 6 | 7 | ![](./sketch_github.gif) 8 | 9 | 10 | This readme is also available in: 11 | 12 | [Welsh](https://github.com/eeue56/elm-sketch-importer/blob/master/README_we.md) 13 | 14 | ## Installation 15 | 16 | We use elm-format in order to format the output correctly. It is recommeneded that you have this installed globally, via 17 | 18 | ``` 19 | npm install -g elm-format 20 | ``` 21 | 22 | Installing the elm-sketch-importer itself is pretty easy. 23 | 24 | ``` 25 | npm install -g elm-sketch-importer 26 | ``` 27 | 28 | ## Usage 29 | 30 | 31 | ``` 32 | Usage: [sketchfile] 33 | 34 | Options: 35 | -h, --help Show help [boolean] 36 | -o, --output Configure the output directory for generated Elm 37 | [default: "generated"] 38 | --ef, --elmformat Specify the location of the elm-format binary 39 | [default: "elm-format"] 40 | 41 | Examples: 42 | elm-sketch-importer ~/Documents/example.sketch 43 | ``` 44 | 45 | 46 | ## Support 47 | 48 | ## Layers 49 | 50 | ### Shapes and rects 51 | 52 | | Feature | Supported? | 53 | |---------|-------------| 54 | | Rectangles | :white_check_mark: | 55 | | Layer positions | :white_check_mark: | 56 | | Layer sizes | :white_check_mark: | 57 | | Multiple layers | :white_check_mark: | 58 | | Fills | :white_check_mark: | 59 | | Border | :warning: | 60 | | Border color | :warning: | 61 | | Colored fills | :white_check_mark: | 62 | | Other shapes | :warning: | 63 | | Groups | :warning: | 64 | | Slices | :warning: | 65 | | Images | :white_check_mark: | 66 | 67 | 68 | ### Text 69 | 70 | Right now, long text is not correctly exported. This is down to the fact that BPLists are a little difficult to parse in Elm. Support will be coming soon, once I've finished the parser! 71 | 72 | | Feature | Supported? | 73 | |---------|------------| 74 | | Short text | :white_check_mark: | 75 | | Long text | :warning: | 76 | | Horizional/vertical flips | :white_check_mark: | 77 | | Position | :white_check_mark: | 78 | | Size | :warning: | 79 | | Color | :warning: | 80 | | Font | :warning: | 81 | 82 | 83 | ## Roadmap 84 | 85 | This roadmap intends to be a rough priority list. No dates nor time are fixed -- but the more PRs to help, the faster things get done :) 86 | 87 | # Before 1.0 88 | 89 | - Relative layout instead of fixed pixels 90 | - Full support for importing Sketch files 91 | - Make sure that no features remain unsupported 92 | 93 | # After 1.0 94 | 95 | - Export views to Sketch 96 | 97 | # Misc 98 | 99 | - Generating elm-css or style-element based views 100 | 101 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at eeue56. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const yargs = require('yargs'); 4 | 5 | const argv = yargs 6 | .usage('Usage: [sketchfile]') 7 | .help('h') 8 | .alias('h', 'help') 9 | .example('~/Documents/example.sketch') 10 | .alias('o', 'output') 11 | .describe('o', 'Configure the output directory for generated Elm') 12 | .default('o', 'generated') 13 | .alias('ef', 'elmformat') 14 | .describe('elmformat', 'Specify the location of the elm-format binary') 15 | .default('elmformat', 'elm-format') 16 | .argv; 17 | 18 | const fs = require('fs'); 19 | const path = require('path'); 20 | const decompress = require('decompress'); 21 | const ProgressBar = require("progress"); 22 | const childProcess = require("child_process"); 23 | const Elm = require('./elm.js'); 24 | 25 | 26 | if (argv._.length === 0){ 27 | console.log('Please provide a file to convert to Elm!'); 28 | process.exit(1); 29 | } 30 | 31 | const options = { 32 | files: argv._, 33 | rootDir: argv.output, 34 | elmFormatPath: argv.ef 35 | }; 36 | 37 | function createOutputDirectory(rootDir) { 38 | try { 39 | fs.mkdirSync(rootDir); 40 | } catch (e){} 41 | 42 | try { 43 | fs.mkdirSync(path.join(rootDir, "images")); 44 | } catch (e) {} 45 | } 46 | 47 | 48 | 49 | function main () { 50 | createOutputDirectory(options.rootDir); 51 | var elmApp = Elm.Runner.worker(); 52 | var pageCount = 0; 53 | 54 | decompress(options.files[0]).then(function(files){ 55 | // send in image file names to elm 56 | const imageFiles = files 57 | .filter(function(file) { return file.path.indexOf("images") === 0 }) 58 | .map(function(file) { return file.path }); 59 | 60 | elmApp.ports.knownImages.send(imageFiles); 61 | 62 | // grab the page files 63 | const pageFiles = files 64 | .filter(function(file) { return file.path.indexOf("pages") === 0}); 65 | 66 | // setup a progress bar going from 0 to the number of pages 67 | const progressBar = new ProgressBar( 68 | "Generating pages: :page [:bar] :percent", 69 | { total: pageFiles.length } 70 | ); 71 | 72 | // respond to content from Elm 73 | elmApp.ports.respond.subscribe(function(data){ 74 | pageCount++; 75 | const pageName = `Page${pageCount}.elm`; 76 | const outputLocation = path.join(options.rootDir, pageName); 77 | fs.writeFile(outputLocation, data, function(){ 78 | childProcess.execSync(options.elmFormatPath + " --yes " + outputLocation, 79 | { stdio: [] } 80 | ); 81 | progressBar.tick({ page: pageName }); 82 | }); 83 | }); 84 | 85 | // send our data over to Elm to parse 86 | pageFiles.forEach(function(file){ 87 | elmApp.ports.parse.send(file.data.toString('utf-8')); 88 | }); 89 | 90 | // write the images 91 | files 92 | .filter(function(file) { return file.path.indexOf("images") === 0}) 93 | .forEach(function(file){ 94 | fs.writeFileSync(path.join(options.rootDir, file.path), file.data); 95 | }); 96 | 97 | console.log(`Done! Generated files at ${path.join(__dirname, options.rootDir)}`); 98 | }).catch(function(err){ 99 | console.error(err); 100 | }); 101 | 102 | } 103 | 104 | main(); -------------------------------------------------------------------------------- /src/Layer.elm: -------------------------------------------------------------------------------- 1 | module Layer exposing (..) 2 | 3 | {-| This module contains the types and decoders for layers. 4 | Most of the heavy work of generating is taken care of in `Render` 5 | 6 | -} 7 | 8 | import Json.Decode as Json 9 | import Json.Encode 10 | import Base64 11 | 12 | 13 | type alias ShapeGroup = 14 | {} 15 | 16 | 17 | type alias Frame = 18 | { height : Float 19 | , width : Float 20 | , x : Float 21 | , y : Float 22 | } 23 | 24 | 25 | type alias Color = 26 | { alpha : Float 27 | , blue : Float 28 | , green : Float 29 | , red : Float 30 | } 31 | 32 | 33 | type alias Fill = 34 | { color : Color } 35 | 36 | 37 | type alias ShapeStyling = 38 | { fills : List Fill } 39 | 40 | 41 | type alias TextStyling = 42 | {} 43 | 44 | 45 | type Style 46 | = ShapeStyle ShapeStyling 47 | | TextStyle TextStyling 48 | 49 | 50 | type alias LayerProps = 51 | { frame : Frame 52 | , style : Style 53 | } 54 | 55 | 56 | type alias Text = 57 | { name : String 58 | , isFlippedVertical : Bool 59 | , isFlippedHorizontal : Bool 60 | , attributedString : String 61 | } 62 | 63 | 64 | type alias ImageProps = 65 | { name : String 66 | , src : String 67 | } 68 | 69 | 70 | type Layer 71 | = GroupLayer (List Layer) 72 | | ShapeGroupLayer LayerProps ShapeGroup 73 | | TextLayer LayerProps Text 74 | | Unknown Json.Value 75 | | BitmapLayer LayerProps ImageProps 76 | | Slice 77 | 78 | 79 | decodeBase64 : Json.Decoder String 80 | decodeBase64 = 81 | Json.string 82 | |> Json.andThen 83 | (\str -> 84 | case Base64.decode str of 85 | Err e -> 86 | Json.fail e 87 | 88 | Ok str -> 89 | Json.succeed str 90 | ) 91 | 92 | 93 | decodeLayer : Json.Decoder Layer 94 | decodeLayer = 95 | Json.field "_class" Json.string 96 | |> Json.andThen 97 | (\class -> 98 | case class of 99 | "shapeGroup" -> 100 | decodeShapeGroupLayer 101 | 102 | "text" -> 103 | decodeTextLayer 104 | 105 | "group" -> 106 | decodeGroupLayer 107 | 108 | "artboard" -> 109 | decodeGroupLayer 110 | 111 | "bitmap" -> 112 | decodeBitmapLayer 113 | 114 | "slice" -> 115 | Json.succeed Slice 116 | 117 | _ -> 118 | Unknown (Json.Encode.string class) 119 | |> Json.succeed 120 | ) 121 | 122 | 123 | decodeGroupLayer : Json.Decoder Layer 124 | decodeGroupLayer = 125 | Json.map GroupLayer (Json.lazy (\_ -> Json.field "layers" <| Json.list decodeLayer)) 126 | 127 | 128 | decodeShapeGroupLayer : Json.Decoder Layer 129 | decodeShapeGroupLayer = 130 | Json.map2 ShapeGroupLayer decodeLayerProps (Json.succeed {}) 131 | 132 | 133 | decodeTextLayer : Json.Decoder Layer 134 | decodeTextLayer = 135 | Json.map2 TextLayer decodeLayerProps decodeText 136 | 137 | 138 | decodeBitmapLayer : Json.Decoder Layer 139 | decodeBitmapLayer = 140 | Json.map2 BitmapLayer decodeLayerProps decodeImageProps 141 | 142 | 143 | decodeText : Json.Decoder Text 144 | decodeText = 145 | Json.map4 Text 146 | (Json.field "name" Json.string) 147 | (Json.field "isFlippedVertical" Json.bool) 148 | (Json.field "isFlippedHorizontal" Json.bool) 149 | (Json.field "attributedString" <| Json.field "archivedAttributedString" <| Json.field "_archive" decodeBase64) 150 | 151 | 152 | decodeImageProps : Json.Decoder ImageProps 153 | decodeImageProps = 154 | Json.map2 ImageProps 155 | (Json.field "name" Json.string) 156 | (Json.field "image" <| Json.field "_ref" Json.string) 157 | 158 | 159 | decodeLayerProps : Json.Decoder LayerProps 160 | decodeLayerProps = 161 | Json.map2 LayerProps 162 | (Json.field "frame" decodeFrame) 163 | (Json.field "style" decodeStyle) 164 | 165 | 166 | decodeFrame : Json.Decoder Frame 167 | decodeFrame = 168 | Json.map4 Frame 169 | (Json.field "height" Json.float) 170 | (Json.field "width" Json.float) 171 | (Json.field "x" Json.float) 172 | (Json.field "y" Json.float) 173 | 174 | 175 | decodeColor : Json.Decoder Color 176 | decodeColor = 177 | Json.map4 Color 178 | (Json.field "alpha" Json.float) 179 | (Json.field "blue" Json.float) 180 | (Json.field "green" Json.float) 181 | (Json.field "red" Json.float) 182 | 183 | 184 | decodeFill : Json.Decoder Fill 185 | decodeFill = 186 | Json.map Fill 187 | (Json.field "color" decodeColor) 188 | 189 | 190 | decodeShapeStyling : Json.Decoder ShapeStyling 191 | decodeShapeStyling = 192 | Json.map ShapeStyling 193 | (Json.field "fills" <| Json.list decodeFill) 194 | 195 | 196 | decodeStyle : Json.Decoder Style 197 | decodeStyle = 198 | Json.oneOf 199 | [ Json.map ShapeStyle decodeShapeStyling 200 | , Json.map TextStyle <| Json.succeed {} 201 | ] 202 | 203 | 204 | {-| Turns the given layer into some useful debug info in the form of text 205 | -} 206 | debugLayer : Layer -> String 207 | debugLayer layer = 208 | case layer of 209 | Unknown stuff -> 210 | "Unknown" 211 | 212 | Slice -> 213 | "Unsupported: slice" 214 | 215 | ShapeGroupLayer props group -> 216 | "" 217 | 218 | TextLayer props text -> 219 | "Text " ++ text.attributedString 220 | 221 | GroupLayer layers -> 222 | List.map debugLayer layers 223 | |> String.join ", " 224 | |> (++) "Layer\n" 225 | 226 | BitmapLayer layer image -> 227 | "Image " ++ image.name 228 | -------------------------------------------------------------------------------- /example/SketchExample.elm: -------------------------------------------------------------------------------- 1 | module SketchExample exposing (..) 2 | 3 | 4 | json : String 5 | json = 6 | """ 7 | { 8 | "_class": "page", 9 | "do_objectID": "46693D5D-991E-49BA-8E28-C1568A1E6428", 10 | "exportOptions": { 11 | "_class": "exportOptions", 12 | "exportFormats": [], 13 | "includedLayerIds": [], 14 | "layerOptions": 0, 15 | "shouldTrim": false 16 | }, 17 | "frame": { 18 | "_class": "rect", 19 | "constrainProportions": false, 20 | "height": 300, 21 | "width": 300, 22 | "x": 0, 23 | "y": 0 24 | }, 25 | "isFlippedHorizontal": false, 26 | "isFlippedVertical": false, 27 | "isLocked": false, 28 | "isVisible": true, 29 | "layerListExpandedType": 0, 30 | "name": "Page 1", 31 | "nameIsFixed": false, 32 | "resizingConstraint": 63, 33 | "resizingType": 0, 34 | "rotation": 0, 35 | "shouldBreakMaskChain": false, 36 | "style": { 37 | "_class": "style", 38 | "endDecorationType": 0, 39 | "miterLimit": 10, 40 | "startDecorationType": 0 41 | }, 42 | "hasClickThrough": true, 43 | "layers": [ 44 | { 45 | "_class": "slice", 46 | "do_objectID": "C3158A4A-B93A-4E99-92BB-FC0638573C5F", 47 | "exportOptions": { 48 | "_class": "exportOptions", 49 | "exportFormats": [ 50 | { 51 | "_class": "exportFormat", 52 | "absoluteSize": 0, 53 | "fileFormat": "png", 54 | "name": "", 55 | "namingScheme": 0, 56 | "scale": 1, 57 | "visibleScaleType": 0 58 | } 59 | ], 60 | "includedLayerIds": [], 61 | "layerOptions": 0, 62 | "shouldTrim": false 63 | }, 64 | "frame": { 65 | "_class": "rect", 66 | "constrainProportions": false, 67 | "height": 291, 68 | "width": 356, 69 | "x": 316, 70 | "y": 116 71 | }, 72 | "isFlippedHorizontal": false, 73 | "isFlippedVertical": false, 74 | "isLocked": false, 75 | "isVisible": true, 76 | "layerListExpandedType": 0, 77 | "name": "Untitled", 78 | "nameIsFixed": false, 79 | "resizingConstraint": 63, 80 | "resizingType": 0, 81 | "rotation": 0, 82 | "shouldBreakMaskChain": false, 83 | "backgroundColor": { 84 | "_class": "color", 85 | "alpha": 1, 86 | "blue": 1, 87 | "green": 1, 88 | "red": 1 89 | }, 90 | "hasBackgroundColor": false 91 | }, 92 | { 93 | "_class": "shapeGroup", 94 | "do_objectID": "ED7855A9-690E-46F4-8BA7-6C62420D3993", 95 | "exportOptions": { 96 | "_class": "exportOptions", 97 | "exportFormats": [], 98 | "includedLayerIds": [], 99 | "layerOptions": 0, 100 | "shouldTrim": false 101 | }, 102 | "frame": { 103 | "_class": "rect", 104 | "constrainProportions": false, 105 | "height": 291, 106 | "width": 356, 107 | "x": 316, 108 | "y": 116 109 | }, 110 | "isFlippedHorizontal": false, 111 | "isFlippedVertical": false, 112 | "isLocked": false, 113 | "isVisible": true, 114 | "layerListExpandedType": 1, 115 | "name": "Rectangle", 116 | "nameIsFixed": false, 117 | "resizingConstraint": 63, 118 | "resizingType": 0, 119 | "rotation": 0, 120 | "shouldBreakMaskChain": false, 121 | "style": { 122 | "_class": "style", 123 | "borders": [ 124 | { 125 | "_class": "border", 126 | "isEnabled": true, 127 | "color": { 128 | "_class": "color", 129 | "alpha": 1, 130 | "blue": 0.592, 131 | "green": 0.592, 132 | "red": 0.592 133 | }, 134 | "fillType": 0, 135 | "position": 1, 136 | "thickness": 1 137 | } 138 | ], 139 | "endDecorationType": 0, 140 | "fills": [ 141 | { 142 | "_class": "fill", 143 | "isEnabled": true, 144 | "color": { 145 | "_class": "color", 146 | "alpha": 1, 147 | "blue": 0.04896134399003047, 148 | "green": 0.04896134399003047, 149 | "red": 0.9556228741496599 150 | }, 151 | "fillType": 0, 152 | "noiseIndex": 0, 153 | "noiseIntensity": 0, 154 | "patternFillType": 0, 155 | "patternTileScale": 1 156 | } 157 | ], 158 | "miterLimit": 10, 159 | "startDecorationType": 0 160 | }, 161 | "hasClickThrough": false, 162 | "layers": [ 163 | { 164 | "_class": "rectangle", 165 | "do_objectID": "210F93E5-5737-46CF-8BAE-83BEEFAD7BC6", 166 | "exportOptions": { 167 | "_class": "exportOptions", 168 | "exportFormats": [], 169 | "includedLayerIds": [], 170 | "layerOptions": 0, 171 | "shouldTrim": false 172 | }, 173 | "frame": { 174 | "_class": "rect", 175 | "constrainProportions": false, 176 | "height": 291, 177 | "width": 356, 178 | "x": 0, 179 | "y": 0 180 | }, 181 | "isFlippedHorizontal": false, 182 | "isFlippedVertical": false, 183 | "isLocked": false, 184 | "isVisible": true, 185 | "layerListExpandedType": 0, 186 | "name": "Path", 187 | "nameIsFixed": false, 188 | "resizingConstraint": 63, 189 | "resizingType": 0, 190 | "rotation": 0, 191 | "shouldBreakMaskChain": false, 192 | "booleanOperation": -1, 193 | "edited": false, 194 | "path": { 195 | "_class": "path", 196 | "isClosed": true, 197 | "pointRadiusBehaviour": 1, 198 | "points": [ 199 | { 200 | "_class": "curvePoint", 201 | "cornerRadius": 0, 202 | "curveFrom": "{0, 0}", 203 | "curveMode": 1, 204 | "curveTo": "{0, 0}", 205 | "hasCurveFrom": false, 206 | "hasCurveTo": false, 207 | "point": "{0, 0}" 208 | }, 209 | { 210 | "_class": "curvePoint", 211 | "cornerRadius": 0, 212 | "curveFrom": "{1, 0}", 213 | "curveMode": 1, 214 | "curveTo": "{1, 0}", 215 | "hasCurveFrom": false, 216 | "hasCurveTo": false, 217 | "point": "{1, 0}" 218 | }, 219 | { 220 | "_class": "curvePoint", 221 | "cornerRadius": 0, 222 | "curveFrom": "{1, 1}", 223 | "curveMode": 1, 224 | "curveTo": "{1, 1}", 225 | "hasCurveFrom": false, 226 | "hasCurveTo": false, 227 | "point": "{1, 1}" 228 | }, 229 | { 230 | "_class": "curvePoint", 231 | "cornerRadius": 0, 232 | "curveFrom": "{0, 1}", 233 | "curveMode": 1, 234 | "curveTo": "{0, 1}", 235 | "hasCurveFrom": false, 236 | "hasCurveTo": false, 237 | "point": "{0, 1}" 238 | } 239 | ] 240 | }, 241 | "fixedRadius": 0, 242 | "hasConvertedToNewRoundCorners": true 243 | } 244 | ], 245 | "clippingMaskMode": 0, 246 | "hasClippingMask": false, 247 | "windingRule": 1 248 | }, 249 | { 250 | "_class": "text", 251 | "do_objectID": "9319567B-CB5F-445A-B726-B13E54F4F963", 252 | "exportOptions": { 253 | "_class": "exportOptions", 254 | "exportFormats": [], 255 | "includedLayerIds": [], 256 | "layerOptions": 0, 257 | "shouldTrim": false 258 | }, 259 | "frame": { 260 | "_class": "rect", 261 | "constrainProportions": false, 262 | "height": 58, 263 | "width": 87.56640625, 264 | "x": 397, 265 | "y": 207 266 | }, 267 | "isFlippedHorizontal": false, 268 | "isFlippedVertical": false, 269 | "isLocked": false, 270 | "isVisible": true, 271 | "layerListExpandedType": 0, 272 | "name": "Hello World", 273 | "nameIsFixed": false, 274 | "resizingConstraint": 63, 275 | "resizingType": 0, 276 | "rotation": 0, 277 | "shouldBreakMaskChain": false, 278 | "style": { 279 | "_class": "style", 280 | "endDecorationType": 0, 281 | "miterLimit": 10, 282 | "startDecorationType": 0, 283 | "textStyle": { 284 | "_class": "textStyle", 285 | "encodedAttributes": { 286 | "NSColor": { 287 | "_archive": "YnBsaXN0MDDUAQIDBAUGFRZYJHZlcnNpb25YJG9iamVjdHNZJGFyY2hpdmVyVCR0b3ASAAGGoKMHCA9VJG51bGzTCQoLDA0OV05TV2hpdGVcTlNDb2xvclNwYWNlViRjbGFzc0IwABADgALSEBESE1okY2xhc3NuYW1lWCRjbGFzc2VzV05TQ29sb3KiEhRYTlNPYmplY3RfEA9OU0tleWVkQXJjaGl2ZXLRFxhUcm9vdIABCBEaIy0yNztBSFBdZGdpa3B7hIyPmKqtsgAAAAAAAAEBAAAAAAAAABkAAAAAAAAAAAAAAAAAAAC0" 288 | }, 289 | "NSParagraphStyle": { 290 | "_archive": "YnBsaXN0MDDUAQIDBAUGFxhYJHZlcnNpb25YJG9iamVjdHNZJGFyY2hpdmVyVCR0b3ASAAGGoKMHCBFVJG51bGzUCQoLDA0ODxBaTlNUYWJTdG9wc1tOU0FsaWdubWVudF8QH05TQWxsb3dzVGlnaHRlbmluZ0ZvclRydW5jYXRpb25WJGNsYXNzgAAQBBABgALSEhMUFVokY2xhc3NuYW1lWCRjbGFzc2VzXxAQTlNQYXJhZ3JhcGhTdHlsZaIUFlhOU09iamVjdF8QD05TS2V5ZWRBcmNoaXZlctEZGlRyb290gAEIERojLTI3O0FKVWGDioyOkJKXoqu+wcrc3+QAAAAAAAABAQAAAAAAAAAbAAAAAAAAAAAAAAAAAAAA5g==" 291 | }, 292 | "MSAttributedStringFontAttribute": { 293 | "_archive": "YnBsaXN0MDDUAQIDBAUGJidYJHZlcnNpb25YJG9iamVjdHNZJGFyY2hpdmVyVCR0b3ASAAGGoKkHCA0XGBkaGyJVJG51bGzSCQoLDFYkY2xhc3NfEBpOU0ZvbnREZXNjcmlwdG9yQXR0cmlidXRlc4AIgALTDg8JEBMWV05TLmtleXNaTlMub2JqZWN0c6IREoADgASiFBWABYAGgAdfEBNOU0ZvbnRTaXplQXR0cmlidXRlXxATTlNGb250TmFtZUF0dHJpYnV0ZSNAOAAAAAAAAFlIZWx2ZXRpY2HSHB0eH1okY2xhc3NuYW1lWCRjbGFzc2VzXxATTlNNdXRhYmxlRGljdGlvbmFyeaMeICFcTlNEaWN0aW9uYXJ5WE5TT2JqZWN00hwdIyRfEBBOU0ZvbnREZXNjcmlwdG9yoiUhXxAQTlNGb250RGVzY3JpcHRvcl8QD05TS2V5ZWRBcmNoaXZlctEoKVRyb290gAEACAARABoAIwAtADIANwBBAEcATABTAHAAcgB0AHsAgwCOAJEAkwCVAJgAmgCcAJ4AtADKANMA3QDiAO0A9gEMARABHQEmASsBPgFBAVQBZgFpAW4AAAAAAAACAQAAAAAAAAAqAAAAAAAAAAAAAAAAAAABcA==" 294 | } 295 | } 296 | } 297 | }, 298 | "attributedString": { 299 | "_class": "MSAttributedString", 300 | "archivedAttributedString": { 301 | "_archive": "YnBsaXN0MDDUAQIDBAUGVFVYJHZlcnNpb25YJG9iamVjdHNZJGFyY2hpdmVyVCR0b3ASAAGGoK8QFQcIDxAcHR4fJSszNjpCQ0RFRkpOUFUkbnVsbNMJCgsMDQ5YTlNTdHJpbmdWJGNsYXNzXE5TQXR0cmlidXRlc4ACgBSAA1tIZWxsbyBXb3JsZNMREgoTFxtXTlMua2V5c1pOUy5vYmplY3RzoxQVFoAEgAWABqMYGRqAB4AJgAuAE1dOU0NvbG9yXxAQTlNQYXJhZ3JhcGhTdHlsZV8QH01TQXR0cmlidXRlZFN0cmluZ0ZvbnRBdHRyaWJ1dGXTICEKIiMkV05TV2hpdGVcTlNDb2xvclNwYWNlQjAAEAOACNImJygpWiRjbGFzc25hbWVYJGNsYXNzZXNXTlNDb2xvcqIoKlhOU09iamVjdNQsLS4KLzAxMlpOU1RhYlN0b3BzW05TQWxpZ25tZW50XxAfTlNBbGxvd3NUaWdodGVuaW5nRm9yVHJ1bmNhdGlvboAAEAQQAYAK0iYnNDVfEBBOU1BhcmFncmFwaFN0eWxlojQq0go3ODlfEBpOU0ZvbnREZXNjcmlwdG9yQXR0cmlidXRlc4ASgAzTERIKOz5Bojw9gA2ADqI/QIAPgBCAEV8QE05TRm9udFNpemVBdHRyaWJ1dGVfEBNOU0ZvbnROYW1lQXR0cmlidXRlI0A4AAAAAAAAWUhlbHZldGljYdImJ0dIXxATTlNNdXRhYmxlRGljdGlvbmFyeaNHSSpcTlNEaWN0aW9uYXJ50iYnS0xfEBBOU0ZvbnREZXNjcmlwdG9yok0qXxAQTlNGb250RGVzY3JpcHRvctImJ0lPokkq0iYnUVJfEBJOU0F0dHJpYnV0ZWRTdHJpbmeiUypfEBJOU0F0dHJpYnV0ZWRTdHJpbmdfEA9OU0tleWVkQXJjaGl2ZXLRVldUcm9vdIABAAgAEQAaACMALQAyADcATwBVAFwAZQBsAHkAewB9AH8AiwCSAJoApQCpAKsArQCvALMAtQC3ALkAuwDDANYA+AD/AQcBFAEXARkBGwEgASsBNAE8AT8BSAFRAVwBaAGKAYwBjgGQAZIBlwGqAa0BsgHPAdEB0wHaAd0B3wHhAeQB5gHoAeoCAAIWAh8CKQIuAkQCSAJVAloCbQJwAoMCiAKLApACpQKoAr0CzwLSAtcAAAAAAAACAQAAAAAAAABYAAAAAAAAAAAAAAAAAAAC2Q==" 302 | } 303 | }, 304 | "automaticallyDrawOnUnderlyingPath": false, 305 | "dontSynchroniseWithSymbol": false, 306 | "glyphBounds": "{{0, 5}, {61, 48}}", 307 | "heightIsClipped": false, 308 | "lineSpacingBehaviour": 2, 309 | "textBehaviour": 1 310 | }, 311 | { 312 | "_class": "shapeGroup", 313 | "do_objectID": "9DCBE03C-7EAC-434E-8522-E482868C8A54", 314 | "exportOptions": { 315 | "_class": "exportOptions", 316 | "exportFormats": [], 317 | "includedLayerIds": [], 318 | "layerOptions": 0, 319 | "shouldTrim": false 320 | }, 321 | "frame": { 322 | "_class": "rect", 323 | "constrainProportions": false, 324 | "height": 45, 325 | "width": 71, 326 | "x": 416, 327 | "y": 308 328 | }, 329 | "isFlippedHorizontal": false, 330 | "isFlippedVertical": false, 331 | "isLocked": false, 332 | "isVisible": true, 333 | "layerListExpandedType": 1, 334 | "name": "Rectangle 2", 335 | "nameIsFixed": false, 336 | "resizingConstraint": 63, 337 | "resizingType": 0, 338 | "rotation": 0, 339 | "shouldBreakMaskChain": false, 340 | "style": { 341 | "_class": "style", 342 | "borders": [ 343 | { 344 | "_class": "border", 345 | "isEnabled": true, 346 | "color": { 347 | "_class": "color", 348 | "alpha": 1, 349 | "blue": 0.592, 350 | "green": 0.592, 351 | "red": 0.592 352 | }, 353 | "fillType": 0, 354 | "position": 1, 355 | "thickness": 1 356 | } 357 | ], 358 | "endDecorationType": 0, 359 | "fills": [ 360 | { 361 | "_class": "fill", 362 | "isEnabled": true, 363 | "color": { 364 | "_class": "color", 365 | "alpha": 1, 366 | "blue": 0.847, 367 | "green": 0.847, 368 | "red": 0.847 369 | }, 370 | "fillType": 0, 371 | "noiseIndex": 0, 372 | "noiseIntensity": 0, 373 | "patternFillType": 0, 374 | "patternTileScale": 1 375 | } 376 | ], 377 | "miterLimit": 10, 378 | "startDecorationType": 0 379 | }, 380 | "hasClickThrough": false, 381 | "layers": [ 382 | { 383 | "_class": "rectangle", 384 | "do_objectID": "7ADD6906-13E6-4151-90E9-7DEE4F8CE37A", 385 | "exportOptions": { 386 | "_class": "exportOptions", 387 | "exportFormats": [], 388 | "includedLayerIds": [], 389 | "layerOptions": 0, 390 | "shouldTrim": false 391 | }, 392 | "frame": { 393 | "_class": "rect", 394 | "constrainProportions": false, 395 | "height": 45, 396 | "width": 71, 397 | "x": 0, 398 | "y": 0 399 | }, 400 | "isFlippedHorizontal": false, 401 | "isFlippedVertical": false, 402 | "isLocked": false, 403 | "isVisible": true, 404 | "layerListExpandedType": 0, 405 | "name": "Path", 406 | "nameIsFixed": false, 407 | "resizingConstraint": 63, 408 | "resizingType": 0, 409 | "rotation": 0, 410 | "shouldBreakMaskChain": false, 411 | "booleanOperation": -1, 412 | "edited": false, 413 | "path": { 414 | "_class": "path", 415 | "isClosed": true, 416 | "pointRadiusBehaviour": 1, 417 | "points": [ 418 | { 419 | "_class": "curvePoint", 420 | "cornerRadius": 0, 421 | "curveFrom": "{0, 0}", 422 | "curveMode": 1, 423 | "curveTo": "{0, 0}", 424 | "hasCurveFrom": false, 425 | "hasCurveTo": false, 426 | "point": "{0, 0}" 427 | }, 428 | { 429 | "_class": "curvePoint", 430 | "cornerRadius": 0, 431 | "curveFrom": "{1, 0}", 432 | "curveMode": 1, 433 | "curveTo": "{1, 0}", 434 | "hasCurveFrom": false, 435 | "hasCurveTo": false, 436 | "point": "{1, 0}" 437 | }, 438 | { 439 | "_class": "curvePoint", 440 | "cornerRadius": 0, 441 | "curveFrom": "{1, 1}", 442 | "curveMode": 1, 443 | "curveTo": "{1, 1}", 444 | "hasCurveFrom": false, 445 | "hasCurveTo": false, 446 | "point": "{1, 1}" 447 | }, 448 | { 449 | "_class": "curvePoint", 450 | "cornerRadius": 0, 451 | "curveFrom": "{0, 1}", 452 | "curveMode": 1, 453 | "curveTo": "{0, 1}", 454 | "hasCurveFrom": false, 455 | "hasCurveTo": false, 456 | "point": "{0, 1}" 457 | } 458 | ] 459 | }, 460 | "fixedRadius": 0, 461 | "hasConvertedToNewRoundCorners": true 462 | } 463 | ], 464 | "clippingMaskMode": 0, 465 | "hasClippingMask": false, 466 | "windingRule": 1 467 | }, 468 | { 469 | "_class": "text", 470 | "do_objectID": "28E84297-4595-47F0-9592-A3CC797F8EBE", 471 | "exportOptions": { 472 | "_class": "exportOptions", 473 | "exportFormats": [], 474 | "includedLayerIds": [], 475 | "layerOptions": 0, 476 | "shouldTrim": false 477 | }, 478 | "frame": { 479 | "_class": "rect", 480 | "constrainProportions": false, 481 | "height": 29, 482 | "width": 139, 483 | "x": 493, 484 | "y": 146 485 | }, 486 | "isFlippedHorizontal": false, 487 | "isFlippedVertical": true, 488 | "isLocked": false, 489 | "isVisible": true, 490 | "layerListExpandedType": 0, 491 | "name": "Upside down", 492 | "nameIsFixed": false, 493 | "resizingConstraint": 47, 494 | "resizingType": 0, 495 | "rotation": 0, 496 | "shouldBreakMaskChain": false, 497 | "style": { 498 | "_class": "style", 499 | "endDecorationType": 0, 500 | "miterLimit": 10, 501 | "startDecorationType": 0, 502 | "textStyle": { 503 | "_class": "textStyle", 504 | "encodedAttributes": { 505 | "NSColor": { 506 | "_archive": "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" 507 | }, 508 | "NSParagraphStyle": { 509 | "_archive": "YnBsaXN0MDDUAQIDBAUGFxhYJHZlcnNpb25YJG9iamVjdHNZJGFyY2hpdmVyVCR0b3ASAAGGoKMHCBFVJG51bGzUCQoLDA0ODxBaTlNUYWJTdG9wc1tOU0FsaWdubWVudF8QH05TQWxsb3dzVGlnaHRlbmluZ0ZvclRydW5jYXRpb25WJGNsYXNzgAAQBBABgALSEhMUFVokY2xhc3NuYW1lWCRjbGFzc2VzXxAQTlNQYXJhZ3JhcGhTdHlsZaIUFlhOU09iamVjdF8QD05TS2V5ZWRBcmNoaXZlctEZGlRyb290gAEIERojLTI3O0FKVWGDioyOkJKXoqu+wcrc3+QAAAAAAAABAQAAAAAAAAAbAAAAAAAAAAAAAAAAAAAA5g==" 510 | }, 511 | "MSAttributedStringFontAttribute": { 512 | "_archive": "YnBsaXN0MDDUAQIDBAUGJidYJHZlcnNpb25YJG9iamVjdHNZJGFyY2hpdmVyVCR0b3ASAAGGoKkHCA0XGBkaGyJVJG51bGzSCQoLDFYkY2xhc3NfEBpOU0ZvbnREZXNjcmlwdG9yQXR0cmlidXRlc4AIgALTDg8JEBMWV05TLmtleXNaTlMub2JqZWN0c6IREoADgASiFBWABYAGgAdfEBNOU0ZvbnRTaXplQXR0cmlidXRlXxATTlNGb250TmFtZUF0dHJpYnV0ZSNAOAAAAAAAAFlIZWx2ZXRpY2HSHB0eH1okY2xhc3NuYW1lWCRjbGFzc2VzXxATTlNNdXRhYmxlRGljdGlvbmFyeaMeICFcTlNEaWN0aW9uYXJ5WE5TT2JqZWN00hwdIyRfEBBOU0ZvbnREZXNjcmlwdG9yoiUhXxAQTlNGb250RGVzY3JpcHRvcl8QD05TS2V5ZWRBcmNoaXZlctEoKVRyb290gAEACAARABoAIwAtADIANwBBAEcATABTAHAAcgB0AHsAgwCOAJEAkwCVAJgAmgCcAJ4AtADKANMA3QDiAO0A9gEMARABHQEmASsBPgFBAVQBZgFpAW4AAAAAAAACAQAAAAAAAAAqAAAAAAAAAAAAAAAAAAABcA==" 513 | } 514 | } 515 | } 516 | }, 517 | "attributedString": { 518 | "_class": "MSAttributedString", 519 | "archivedAttributedString": { 520 | "_archive": "YnBsaXN0MDDUAQIDBAUGaWpYJHZlcnNpb25YJG9iamVjdHNZJGFyY2hpdmVyVCR0b3ASAAGGoK8QGQcIDxAcHR4fKS8zOj5BSEtPV1hZWltfY2VVJG51bGzTCQoLDA0OWE5TU3RyaW5nViRjbGFzc1xOU0F0dHJpYnV0ZXOAAoAYgANbVXBzaWRlIGRvd27TERIKExcbV05TLmtleXNaTlMub2JqZWN0c6MUFRaABIAFgAajGBkagAeADYAPgBdXTlNDb2xvcl8QEE5TUGFyYWdyYXBoU3R5bGVfEB9NU0F0dHJpYnV0ZWRTdHJpbmdGb250QXR0cmlidXRl1SAhIiMKJCUmJyhcTlNDb21wb25lbnRzVU5TUkdCXE5TQ29sb3JTcGFjZV8QEk5TQ3VzdG9tQ29sb3JTcGFjZU8QKjAuMDAyNDI3MDEyNDcyIDAuMDkyMzE0ODM3NCAwLjg5Nzk1OTE4MzcgMU8QETAgMCAwLjg3MTU5MTMyOTYAEAGACIAM0yorCiwtLlROU0lEVU5TSUNDEAeACYAL0jAKMTJXTlMuZGF0YU8RDEgAAAxITGlubwIQAABtbnRyUkdCIFhZWiAHzgACAAkABgAxAABhY3NwTVNGVAAAAABJRUMgc1JHQgAAAAAAAAAAAAAAAAAA9tYAAQAAAADTLUhQICAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABFjcHJ0AAABUAAAADNkZXNjAAABhAAAAGx3dHB0AAAB8AAAABRia3B0AAACBAAAABRyWFlaAAACGAAAABRnWFlaAAACLAAAABRiWFlaAAACQAAAABRkbW5kAAACVAAAAHBkbWRkAAACxAAAAIh2dWVkAAADTAAAAIZ2aWV3AAAD1AAAACRsdW1pAAAD+AAAABRtZWFzAAAEDAAAACR0ZWNoAAAEMAAAAAxyVFJDAAAEPAAACAxnVFJDAAAEPAAACAxiVFJDAAAEPAAACAx0ZXh0AAAAAENvcHlyaWdodCAoYykgMTk5OCBIZXdsZXR0LVBhY2thcmQgQ29tcGFueQAAZGVzYwAAAAAAAAASc1JHQiBJRUM2MTk2Ni0yLjEAAAAAAAAAAAAAABJzUkdCIElFQzYxOTY2LTIuMQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWFlaIAAAAAAAAPNRAAEAAAABFsxYWVogAAAAAAAAAAAAAAAAAAAAAFhZWiAAAAAAAABvogAAOPUAAAOQWFlaIAAAAAAAAGKZAAC3hQAAGNpYWVogAAAAAAAAJKAAAA+EAAC2z2Rlc2MAAAAAAAAAFklFQyBodHRwOi8vd3d3LmllYy5jaAAAAAAAAAAAAAAAFklFQyBodHRwOi8vd3d3LmllYy5jaAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABkZXNjAAAAAAAAAC5JRUMgNjE5NjYtMi4xIERlZmF1bHQgUkdCIGNvbG91ciBzcGFjZSAtIHNSR0IAAAAAAAAAAAAAAC5JRUMgNjE5NjYtMi4xIERlZmF1bHQgUkdCIGNvbG91ciBzcGFjZSAtIHNSR0IAAAAAAAAAAAAAAAAAAAAAAAAAAAAAZGVzYwAAAAAAAAAsUmVmZXJlbmNlIFZpZXdpbmcgQ29uZGl0aW9uIGluIElFQzYxOTY2LTIuMQAAAAAAAAAAAAAALFJlZmVyZW5jZSBWaWV3aW5nIENvbmRpdGlvbiBpbiBJRUM2MTk2Ni0yLjEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHZpZXcAAAAAABOk/gAUXy4AEM8UAAPtzAAEEwsAA1yeAAAAAVhZWiAAAAAAAEwJVgBQAAAAVx/nbWVhcwAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAo8AAAACc2lnIAAAAABDUlQgY3VydgAAAAAAAAQAAAAABQAKAA8AFAAZAB4AIwAoAC0AMgA3ADsAQABFAEoATwBUAFkAXgBjAGgAbQByAHcAfACBAIYAiwCQAJUAmgCfAKQAqQCuALIAtwC8AMEAxgDLANAA1QDbAOAA5QDrAPAA9gD7AQEBBwENARMBGQEfASUBKwEyATgBPgFFAUwBUgFZAWABZwFuAXUBfAGDAYsBkgGaAaEBqQGxAbkBwQHJAdEB2QHhAekB8gH6AgMCDAIUAh0CJgIvAjgCQQJLAlQCXQJnAnECegKEAo4CmAKiAqwCtgLBAssC1QLgAusC9QMAAwsDFgMhAy0DOANDA08DWgNmA3IDfgOKA5YDogOuA7oDxwPTA+AD7AP5BAYEEwQgBC0EOwRIBFUEYwRxBH4EjASaBKgEtgTEBNME4QTwBP4FDQUcBSsFOgVJBVgFZwV3BYYFlgWmBbUFxQXVBeUF9gYGBhYGJwY3BkgGWQZqBnsGjAadBq8GwAbRBuMG9QcHBxkHKwc9B08HYQd0B4YHmQesB78H0gflB/gICwgfCDIIRghaCG4IggiWCKoIvgjSCOcI+wkQCSUJOglPCWQJeQmPCaQJugnPCeUJ+woRCicKPQpUCmoKgQqYCq4KxQrcCvMLCwsiCzkLUQtpC4ALmAuwC8gL4Qv5DBIMKgxDDFwMdQyODKcMwAzZDPMNDQ0mDUANWg10DY4NqQ3DDd4N+A4TDi4OSQ5kDn8Omw62DtIO7g8JDyUPQQ9eD3oPlg+zD88P7BAJECYQQxBhEH4QmxC5ENcQ9RETETERTxFtEYwRqhHJEegSBxImEkUSZBKEEqMSwxLjEwMTIxNDE2MTgxOkE8UT5RQGFCcUSRRqFIsUrRTOFPAVEhU0FVYVeBWbFb0V4BYDFiYWSRZsFo8WshbWFvoXHRdBF2UXiReuF9IX9xgbGEAYZRiKGK8Y1Rj6GSAZRRlrGZEZtxndGgQaKhpRGncanhrFGuwbFBs7G2MbihuyG9ocAhwqHFIcexyjHMwc9R0eHUcdcB2ZHcMd7B4WHkAeah6UHr4e6R8THz4faR+UH78f6iAVIEEgbCCYIMQg8CEcIUghdSGhIc4h+yInIlUigiKvIt0jCiM4I2YjlCPCI/AkHyRNJHwkqyTaJQklOCVoJZclxyX3JicmVyaHJrcm6CcYJ0kneierJ9woDSg/KHEooijUKQYpOClrKZ0p0CoCKjUqaCqbKs8rAis2K2krnSvRLAUsOSxuLKIs1y0MLUEtdi2rLeEuFi5MLoIuty7uLyQvWi+RL8cv/jA1MGwwpDDbMRIxSjGCMbox8jIqMmMymzLUMw0zRjN/M7gz8TQrNGU0njTYNRM1TTWHNcI1/TY3NnI2rjbpNyQ3YDecN9c4FDhQOIw4yDkFOUI5fzm8Ofk6Njp0OrI67zstO2s7qjvoPCc8ZTykPOM9Ij1hPaE94D4gPmA+oD7gPyE/YT+iP+JAI0BkQKZA50EpQWpBrEHuQjBCckK1QvdDOkN9Q8BEA0RHRIpEzkUSRVVFmkXeRiJGZ0arRvBHNUd7R8BIBUhLSJFI10kdSWNJqUnwSjdKfUrESwxLU0uaS+JMKkxyTLpNAk1KTZNN3E4lTm5Ot08AT0lPk0/dUCdQcVC7UQZRUFGbUeZSMVJ8UsdTE1NfU6pT9lRCVI9U21UoVXVVwlYPVlxWqVb3V0RXklfgWC9YfVjLWRpZaVm4WgdaVlqmWvVbRVuVW+VcNVyGXNZdJ114XcleGl5sXr1fD19hX7NgBWBXYKpg/GFPYaJh9WJJYpxi8GNDY5dj62RAZJRk6WU9ZZJl52Y9ZpJm6Gc9Z5Nn6Wg/aJZo7GlDaZpp8WpIap9q92tPa6dr/2xXbK9tCG1gbbluEm5rbsRvHm94b9FwK3CGcOBxOnGVcfByS3KmcwFzXXO4dBR0cHTMdSh1hXXhdj52m3b4d1Z3s3gReG54zHkqeYl553pGeqV7BHtje8J8IXyBfOF9QX2hfgF+Yn7CfyN/hH/lgEeAqIEKgWuBzYIwgpKC9INXg7qEHYSAhOOFR4Wrhg6GcobXhzuHn4gEiGmIzokziZmJ/opkisqLMIuWi/yMY4zKjTGNmI3/jmaOzo82j56QBpBukNaRP5GokhGSepLjk02TtpQglIqU9JVflcmWNJaflwqXdZfgmEyYuJkkmZCZ/JpomtWbQpuvnByciZz3nWSd0p5Anq6fHZ+Ln/qgaaDYoUehtqImopajBqN2o+akVqTHpTilqaYapoum/adup+CoUqjEqTepqaocqo+rAqt1q+msXKzQrUStuK4trqGvFq+LsACwdbDqsWCx1rJLssKzOLOutCW0nLUTtYq2AbZ5tvC3aLfguFm40blKucK6O7q1uy67p7whvJu9Fb2Pvgq+hL7/v3q/9cBwwOzBZ8Hjwl/C28NYw9TEUcTOxUvFyMZGxsPHQce/yD3IvMk6ybnKOMq3yzbLtsw1zLXNNc21zjbOts83z7jQOdC60TzRvtI/0sHTRNPG1EnUy9VO1dHWVdbY11zX4Nhk2OjZbNnx2nba+9uA3AXcit0Q3ZbeHN6i3ynfr+A24L3hROHM4lPi2+Nj4+vkc+T85YTmDeaW5x/nqegy6LzpRunQ6lvq5etw6/vshu0R7ZzuKO6070DvzPBY8OXxcvH/8ozzGfOn9DT0wvVQ9d72bfb794r4Gfio+Tj5x/pX+uf7d/wH/Jj9Kf26/kv+3P9t//+ACtI0NTY3WiRjbGFzc25hbWVYJGNsYXNzZXNdTlNNdXRhYmxlRGF0YaM2ODlWTlNEYXRhWE5TT2JqZWN00jQ1OzxcTlNDb2xvclNwYWNloj05XE5TQ29sb3JTcGFjZdI0NT9AV05TQ29sb3KiPznUQkNECkVGJkdaTlNUYWJTdG9wc1tOU0FsaWdubWVudF8QH05TQWxsb3dzVGlnaHRlbmluZ0ZvclRydW5jYXRpb26AABAEgA7SNDVJSl8QEE5TUGFyYWdyYXBoU3R5bGWiSTnSCkxNTl8QGk5TRm9udERlc2NyaXB0b3JBdHRyaWJ1dGVzgBaAENMREgpQU1aiUVKAEYASolRVgBOAFIAVXxATTlNGb250U2l6ZUF0dHJpYnV0ZV8QE05TRm9udE5hbWVBdHRyaWJ1dGUjQDgAAAAAAABZSGVsdmV0aWNh0jQ1XF1fEBNOU011dGFibGVEaWN0aW9uYXJ5o1xeOVxOU0RpY3Rpb25hcnnSNDVgYV8QEE5TRm9udERlc2NyaXB0b3KiYjlfEBBOU0ZvbnREZXNjcmlwdG9y0jQ1XmSiXjnSNDVmZ18QEk5TQXR0cmlidXRlZFN0cmluZ6JoOV8QEk5TQXR0cmlidXRlZFN0cmluZ18QD05TS2V5ZWRBcmNoaXZlctFrbFRyb290gAEACAARABoAIwAtADIANwBTAFkAYABpAHAAfQB/AIEAgwCPAJYAngCpAK0ArwCxALMAtwC5ALsAvQC/AMcA2gD8AQcBFAEaAScBPAFpAX0BfwGBAYMBigGPAZUBlwGZAZsBoAGoDfQN9g37DgYODw4dDiEOKA4xDjYOQw5GDlMOWA5gDmMObA53DoMOpQ6nDqkOqw6wDsMOxg7LDugO6g7sDvMO9g74DvoO/Q7/DwEPAw8ZDy8POA9CD0cPXQ9hD24Pcw+GD4kPnA+hD6QPqQ++D8EP1g/oD+sP8AAAAAAAAAIBAAAAAAAAAG0AAAAAAAAAAAAAAAAAAA/y" 521 | } 522 | }, 523 | "automaticallyDrawOnUnderlyingPath": false, 524 | "dontSynchroniseWithSymbol": false, 525 | "glyphBounds": "{{1, 5}, {137, 24}}", 526 | "heightIsClipped": false, 527 | "lineSpacingBehaviour": 2, 528 | "textBehaviour": 0 529 | } 530 | ], 531 | "horizontalRulerData": { 532 | "_class": "rulerData", 533 | "base": 0, 534 | "guides": [] 535 | }, 536 | "includeInCloudUpload": true, 537 | "verticalRulerData": { 538 | "_class": "rulerData", 539 | "base": 0, 540 | "guides": [] 541 | } 542 | } 543 | """ 544 | --------------------------------------------------------------------------------