(msg :T) {
174 | // send message to ui
175 | figma.ui.postMessage(msg)
176 | }
177 |
178 |
179 | function main() {
180 | figma.showUI(__html__, {
181 | width: 440,
182 | height: 600,
183 | })
184 |
185 | figma.ui.onmessage = msg => {
186 | switch (msg.type) {
187 |
188 | case "update-graph":
189 | onUpdateGraph(msg as UpdateGraphMsg)
190 | break
191 |
192 | case "error":
193 | onUIError(msg as ErrorMsg)
194 | break
195 |
196 | case "close-plugin":
197 | figma.closePlugin()
198 | break
199 |
200 | default:
201 | console.warn(`plugin received unexpected message`, msg)
202 | break
203 | }
204 | }
205 |
206 | figma.on("selectionchange", updateSelectedGraphFrame)
207 |
208 | // initial check
209 | updateSelectedGraphFrame()
210 | }
211 |
212 |
213 | main()
214 |
--------------------------------------------------------------------------------
/graphviz/src/structs.ts:
--------------------------------------------------------------------------------
1 | export interface Msg {
2 | type :string
3 | }
4 |
5 | export interface UpdateGraphMsg extends Msg {
6 | type :"update-graph"
7 | reqId :number
8 | svgCode :string
9 | sourceCode :string
10 | forceInsertNew :bool // don't attempt to replace exisiting graph
11 | }
12 |
13 | export interface ResponseMsg extends Msg {
14 | type :"response"
15 | reqId :number
16 | error? :string
17 | }
18 |
19 | export interface ErrorMsg extends Msg {
20 | type: "error"
21 | error: string
22 | }
23 |
24 | export interface ClosePluginMsg extends Msg {
25 | type: "close-plugin"
26 | }
27 |
28 | export interface UpdateUIMsg extends Msg {
29 | type: "update-ui"
30 | nodeId :string // non-empty when a graph node is selected
31 | sourceCode :string // valid when nodeId is set
32 | }
33 |
--------------------------------------------------------------------------------
/graphviz/src/ui.css:
--------------------------------------------------------------------------------
1 | @import url("https://rsms.me/inter/inter.css");
2 | @import url("https://rsms.me/res/fonts/iaw.css");
3 |
4 | /* reset */
5 | * { font-family: inherit; line-height: inherit; font-synthesis: none; }
6 | a, abbr, acronym, address, applet, article, aside, audio, b, big, blockquote,
7 | body, canvas, caption, center, cite, code, dd, del, details, dfn, div, dl, dt,
8 | em, embed, fieldset, figcaption, figure, footer, form, grid, h1, h2, h3, h4, h5,
9 | h6, header, hgroup, hr, html, i, iframe, img, ins, kbd, label, legend, li, main,
10 | mark, menu, nav, noscript, object, ol, output, p, pre, q, s, samp, section,
11 | small, span, strike, strong, sub, summary, sup, table, tbody, td, tfoot, th,
12 | thead, time, tr, tt, u, ul, var, video {
13 | margin: 0;
14 | padding: 0;
15 | border: 0;
16 | vertical-align: baseline;
17 | }
18 | blockquote, q { quotes: none; }
19 | blockquote:before, blockquote:after, q:before, q:after {
20 | content: "";
21 | content: none;
22 | }
23 | table {
24 | border-collapse: collapse;
25 | border-spacing: 0;
26 | }
27 | a, a:active, a:visited { color: inherit; }
28 | /* end of reset */
29 |
30 | :root {
31 | --blue: #0085ff;
32 | --fontSize: 12px;
33 | --editorFontSize: var(--fontSize);
34 | --fontFamily: Inter;
35 | --editorFontFamily: 'iaw-quattro';
36 | --toolbarHeight: 40px;
37 | }
38 | @supports (font-variation-settings: normal) {
39 | :root {
40 | --fontFamily: "Inter var";
41 | --editorFontFamily: 'iaw-quattro-var';
42 | }
43 | }
44 |
45 | body {
46 | background: transparent;
47 | color: #222;
48 | font: var(--fontSize)/1.4 var(--fontFamily), system-ui, -system-ui, sans-serif;
49 | display: flex;
50 | flex-direction: column;
51 | }
52 |
53 | textarea {
54 | flex:1 1 auto;
55 | min-height:100px;
56 | font-family: var(--editorFontFamily);
57 | font-size: var(--editorFontSize);
58 | line-height: 1.4;
59 | border: none;
60 | outline: none;
61 | padding: 8px 16px;
62 | resize: none;
63 |
64 | color: #666;
65 |
66 | &:focus {
67 | /*box-shadow: inset 0 0 0 2px rgba(0, 100, 255, 0.2);*/
68 | color: #000;
69 | }
70 | }
71 |
72 | #toolbar {
73 | height: var(--toolbarHeight);
74 | display: flex;
75 | justify-content: space-evenly;
76 | box-shadow: 0 -1px 0 0 rgba(0,0,0,0.1);
77 | z-index: 1;
78 |
79 | & button {
80 | flex: 1 1 50%;
81 | background: none;
82 | border: none;
83 | border-left: 1px solid #e5e5e5;
84 | padding: 0 16px;
85 | line-height: var(--toolbarHeight);
86 | font-weight: 500;
87 | font-size: inherit;
88 | white-space: nowrap;
89 |
90 | &:active {
91 | background: rgba(0,0,0,0.07);
92 | }
93 | &:focus {
94 | color:green;
95 | }
96 |
97 | &:first-child { border: none }
98 | &.primary {
99 | /*font-weight: 600;*/
100 | color: var(--blue);
101 | }
102 | }
103 | }
104 |
105 | @keyframes spin {
106 | 0% { transform: rotate(0deg); }
107 | 100% { transform: rotate(360deg); }
108 | }
109 |
110 | #spinner {
111 | position: fixed;
112 | left:0; top:0; right:0; bottom:48px;
113 | z-index: 9;
114 | pointer-events: none;
115 | display: flex;
116 | align-items: center;
117 | justify-content: center;
118 | opacity: 0;
119 | transition: opacity 100ms ease-in-out;
120 | & > div {
121 | position:relative;
122 | width:24px;
123 | height:24px;
124 | & svg {
125 | position:absolute;
126 | left:0; top:0;
127 | &.shadow {
128 | /* note: we can't use transform here */
129 | margin-top: 5px;
130 | filter: blur(1.5px);
131 | }
132 | &.shadow path {
133 | stroke: rgba(0, 0, 100, 0.1);
134 | }
135 | }
136 | }
137 | }
138 | #spinner.active {
139 | opacity: 1;
140 | transition-delay: 400ms;
141 | transition-duration: 200ms;
142 |
143 | & svg {
144 | animation-name: spin;
145 | animation-duration: 800ms;
146 | animation-iteration-count: infinite;
147 | animation-timing-function: linear;
148 | }
149 | }
150 |
--------------------------------------------------------------------------------
/graphviz/src/ui.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
81 |
84 |
85 |
86 |
87 |
--------------------------------------------------------------------------------
/graphviz/src/ui.ts:
--------------------------------------------------------------------------------
1 | import { Msg, ClosePluginMsg, UpdateUIMsg, UpdateGraphMsg, ResponseMsg, ErrorMsg } from "./structs"
2 | import { Editor } from "./editor"
3 | import { addKeyEventHandler } from "./util"
4 |
5 | // declare function Viz(dotSource :string, outputFormat :string)
6 |
7 | // graphviz module
8 | type GVFormat = "svg" | "dot" | "json" | "dot_json" | "xdot_json";
9 | type GVEngine = "circo" | "dot" | "fdp" | "neato" | "osage" | "patchwork" | "twopi";
10 | const graphviz = window["graphviz"] as {
11 | layout(source :string, format? :GVFormat, engine? :GVEngine, timeout? :number) :Promise
12 | }
13 |
14 |
15 | const isMac = navigator.platform.indexOf("Mac") != -1
16 | const genButton = document.querySelector('button.gen')! as HTMLButtonElement
17 | const playgroundButton = document.querySelector('button.playground')! as HTMLButtonElement
18 | const demoButton = document.querySelector('button.demo')! as HTMLButtonElement
19 | const spinner = document.querySelector('#spinner')! as HTMLDivElement
20 | const editor = new Editor(document.getElementById('dotcode')! as HTMLTextAreaElement)
21 |
22 | // memory-only, since we can't use localStorage in plugins
23 | let untitledSourceCode = editor.defaultText
24 |
25 | // genButton labels
26 | const genButtonLabelCreate = genButton.innerText
27 | const genButtonLabelUpdate = "Update"
28 | const genButtonLabelBusy = "Working"
29 | let genButtonLabel = genButtonLabelCreate
30 |
31 |
32 | const graphDefaults = (
33 | ' graph [fontname="Arial,Inter" bgcolor=transparent];\n' +
34 | ' node [fontname="Arial,Inter"];\n' +
35 | ' edge [fontname="Arial,Inter"];\n'
36 | )
37 |
38 |
39 | function wrapInGraphDirective(s :string) :string {
40 | return (
41 | 'digraph G {\n' +
42 | graphDefaults +
43 | s +
44 | '\n}\n'
45 | )
46 | }
47 |
48 |
49 | async function makeViz(dotSource :string) :Promise {
50 | let svg = ""
51 | let originalDotSource = dotSource
52 |
53 | let addedGraphDirective = false
54 | let m = dotSource.match(/\b(?:di)?graph(?:\s+[^\{]+|)[\r\n\s]*\{/)
55 | if (m) {
56 | // found graph directive -- add defaults
57 | let i = (m.index||0) + m[0].length
58 | dotSource = dotSource.substr(0, i) + "\n" + graphDefaults + dotSource.substr(i)
59 | } else {
60 | // no graph directive -- wrap & add defaults
61 | dotSource = wrapInGraphDirective(dotSource)
62 | addedGraphDirective = true
63 | }
64 |
65 | while (1) {
66 | try {
67 | // svg = Viz(dotSource, "svg")
68 | svg = await graphviz.layout(dotSource, "svg", "dot", 30000)
69 | break
70 | } catch (err) {
71 | if (err.message && (err.message+"").toLowerCase().indexOf("syntax error") != -1) {
72 | if (!addedGraphDirective) {
73 | // try and see if adding graph directive fixes it
74 | dotSource = wrapInGraphDirective(originalDotSource)
75 | addedGraphDirective = true
76 | dlog("makeViz retry with wrapped graph directive. New dotSource:\n" + dotSource)
77 | } else {
78 | throw new Error("malformed dot code")
79 | }
80 | } else {
81 | throw err
82 | }
83 | }
84 | }
85 |
86 | // clean up svg
87 | //
88 | //
89 | //
90 | // ...
91 | // xmlns:xlink="http://www.w3.org/1999/xlink"
92 | // (useless rectangle)
93 | svg = svg.replace(
94 | /<\?xml[^>]+\?>|<\!DOCTYPE[^>]+>|<\!--.*-->|xmlns:xlink="http:\/\/www.w3.org\/1999\/xlink\"/gm,
95 | ""
96 | )
97 |
98 | // remove comments and collapse linebreaks.
99 | // Note that none of the data generated actually has linebreaks, so this is safe.
100 | svg = svg.replace(/[\r\n]+/g, " ").replace(/<\!--.*-->/g, "").trim()
101 |
102 | // remove background rectangle
103 | svg = svg.replace(
104 | /^(