Skip to content

Commit

Permalink
Add JSON export format
Browse files Browse the repository at this point in the history
  • Loading branch information
jolle committed Feb 18, 2024
1 parent 4286774 commit 6a1b186
Show file tree
Hide file tree
Showing 3 changed files with 112 additions and 9 deletions.
39 changes: 37 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Miro board exporter

Exports Miro frames as full-detail SVGs using a headless Puppeteer browser. Requires a personal Miro token.
Exports Miro frames as full-detail SVGs or JSON using a headless Puppeteer browser. Requires a personal Miro token.

## Getting the Miro token

Expand All @@ -14,6 +14,7 @@ Options:
-b, --board-id <boardId> The board ID
-f, --frame-names <frameNames...> The frame name(s), leave empty to export entire board
-o, --output-file <filename> A file to output the SVG to (stdout if not supplied)
-e, --export-format <format> 'svg' or 'json' (default: 'svg')
-h, --help display help for command
```

Expand All @@ -28,8 +29,42 @@ miro-export -t XYZ -b uMoVLkx8gIc=

# export "Frame 2" and "Frame 3" to "Frame 2.svg" and "Frame 3.svg" respectively
miro-export -t XYZ -b uMoVLkx8gIc= -f "Frame 2" "Frame 3" -o "{frameName}.svg"

# export JSON representation of "Frame 2"
miro-export -t XYZ -b uMoVLkx8gIc= -f "Frame 2" -e json
```

## Capturing multiple frames at once

It is possible to supply multiple frames to the `-f` switch, e.g., `-f "Frame 2" "Frame 3"`. However, this will capture all content that is within the outer bounding box when all frames have been selected, so content between the frames will be captured as well. If you want separate SVGs for each frame, use the output file switch with `{frameName}` in the file name, e.g., `-o "Export - {frameName}.svg"`. It is not possible to export separate SVGs without the output file specified (i.e., to stdout).
It is possible to supply multiple frames to the `-f` switch, e.g., `-f "Frame 2" "Frame 3"`. However, for SVG export, this will capture all content that is within the outer bounding box when all frames have been selected, so content between the frames will be captured as well. If you want separate SVGs for each frame, use the output file switch with `{frameName}` in the file name, e.g., `-o "Export - {frameName}.svg"`. It is not possible to export separate SVGs without the output file specified (i.e., to stdout).

## JSON export

The JSON export format is a Miro-internal representation of all the board objects. It is not a documented format, but it is quite easy to understand. The exported format is always an array of objects that have the field `type` as a discriminator. Depending on the type, fields change, but there is always at least an `id` field. For example, a `sticky_note` object could look like this:

```json
{
"type": "sticky_note",
"shape": "square",
"content": "<p>Test content</p>",
"style": {
"fillColor": "cyan",
"textAlign": "center",
"textAlignVertical": "middle"
},
"tagIds": [],
"id": "3458764564249021457",
"parentId": "3458764564247784511",
"origin": "center",
"relativeTo": "parent_top_left",
"createdAt": "2023-09-11T12:45:00.041Z",
"createdBy": "3458764537906310005",
"modifiedAt": "2023-09-11T12:46:01.041Z",
"modifiedBy": "3458764537906310005",
"connectorIds": [],
"x": 129.29101113436059,
"y": 201.25587788616645,
"width": 101.46000000000001,
"height": 125.12
}
```
78 changes: 73 additions & 5 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,41 @@ import { writeFile } from "fs/promises";
import puppeteer from "puppeteer";
import { program } from "@commander-js/extra-typings";

type BoardObjectType = "frame" | "group" | "sticky_note" | "text";
interface BoardObjectBase {
title: string;
id: string;
type: BoardObjectType;
}
interface FrameBoardObject extends BoardObjectBase {
type: "frame";
title: string;
childrenIds: string[];
}
interface GroupBoardObject extends BoardObjectBase {
type: "group";
itemsIds: string[];
}
interface StickyNoteBoardObject extends BoardObjectBase {
type: "sticky_note";
}
interface TextBoardObject extends BoardObjectBase {
type: "text";
}
type BoardObject =
| FrameBoardObject
| GroupBoardObject
| StickyNoteBoardObject
| TextBoardObject;

declare global {
interface Window {
miro: {
board: {
get(opts: {
type: "frame"[];
}): Promise<{ title: string; id: string }[]>;
type?: BoardObjectType[];
id?: string[];
}): Promise<BoardObject[]>;
select(opts: { id: string }): Promise<void>;
deselect(): Promise<void>;
};
Expand All @@ -25,7 +53,7 @@ declare global {
}
}

const { token, boardId, frameNames, outputFile } = program
const { token, boardId, frameNames, outputFile, exportFormat } = program
.requiredOption("-t, --token <token>", "Miro token")
.requiredOption("-b, --board-id <boardId>", "The board ID")
.option(
Expand All @@ -36,6 +64,7 @@ const { token, boardId, frameNames, outputFile } = program
"-o, --output-file <filename>",
"A file to output the SVG to (stdout if not supplied)"
)
.option("-e, --export-format <format>", "'svg' or 'json' (default: 'svg')")
.parse()
.opts();

Expand Down Expand Up @@ -99,6 +128,45 @@ const { token, boardId, frameNames, outputFile } = program
return await window.cmd.board.api.export.makeVector();
}, frameNames);

const getJsonForFrames = (frameNames: string[] | undefined) =>
page.evaluate(async (frameNames) => {
if (frameNames) {
const frames = await window.miro.board.get({ type: ["frame"] });

const selectedFrames = frames.filter((frame) =>
frameNames.includes(frame.title)
);

if (selectedFrames.length !== frameNames.length) {
throw Error(
`${
frameNames.length - selectedFrames.length
} frame(s) could not be found on the board.`
);
}

const children = await window.miro.board.get({
id: selectedFrames.flatMap(
(frame) => (frame as FrameBoardObject).childrenIds
)
});

const groupChildren = await window.miro.board.get({
id: children
.filter(
(child): child is GroupBoardObject => child.type === "group"
)
.flatMap((child) => child.itemsIds)
});

return JSON.stringify([...frames, ...children, ...groupChildren]);
}

return JSON.stringify(await window.miro.board.get({}));
}, frameNames);

const getFn = exportFormat === "json" ? getJsonForFrames : getSvgForFrames;

if (outputFile?.includes("{frameName}")) {
if (!frameNames) {
throw Error(
Expand All @@ -107,11 +175,11 @@ const { token, boardId, frameNames, outputFile } = program
}

for (const frameName of frameNames) {
const svg = await getSvgForFrames([frameName]);
const svg = await getFn([frameName]);
await writeFile(outputFile.replace("{frameName}", frameName), svg);
}
} else {
const svg = await getSvgForFrames(frameNames);
const svg = await getFn(frameNames);
if (outputFile) {
await writeFile(outputFile, svg);
} else {
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
{
"name": "miro-export",
"version": "1.0.0",
"version": "1.0.1",
"author": "jolle <npm-contact@jolle.io>",
"license": "MIT",
"description": "Export Miro boards or frames as SVGs",
"description": "Export Miro boards and/or frames as SVG or JSON",
"keywords": [
"miro",
"svg",
Expand Down

0 comments on commit 6a1b186

Please sign in to comment.