×

Recreating Structured Craft Content in OmniFocus

A step-by-step guide for creating a Craft eXtension that makes elements in OmniFocus based upon structured content selected in Craft.

The following documentation details how to create a custom Craft eXtension for using the content of the currently selected blocks to create a new object (project) in OmniFocus.

Video 1: Transfer Structured Content
Recreates the structured Craft selection in OmniFocus as a project.

DOWNLOAD CRAFT EXTENSION

DOWNLOAD MARKDOWN FILE

STEP 1: Identify the Craft Content to Transfer

Most Craft content can be transfered to Omni applications, it depends entirely on being able to logically parse the selected Craft selected data, and to have objects in the targeted Omni application that can correspond to the passed Craft data.

The Craft source object can be a single block or a set of blocks that can be identified as an entity by their selection.

In order to create automation tools that work consistently with Craft, the source object in Craft must:

In the following illustration, the components of the Craft object to transfer are identified by descriptors placed within parens, like: (TASK)

In the example, a series of selected Craft blocks will be used to create a new new OmniFocus project with defined tasks.

Structured Craft Object

These logical assumptions are made for the example Craft object:

OPTION: Instead of importing the downloaded example markdown file as a new document, you can copy and paste the following markdown content and paste it into the title field of a new Craft document:

Example Markdown Content for New Document


# ::(DOCUMENT TITLE):: Example Project Item + ::(PROJECT TITLE):: **IPSUM CURSUS PORTA** > ::(PROJECT NOTE & LINK TO PROJECT):: Aenean lacinia bibendum nulla sed consectetur. Vestibulum id ligula porta felis euismod semper. Aenean eu leo quam. Pellentesque ornare sem lacinia quam venenatis vestibulum. - [ ] ::(TASK):: Nulla vitae elit libero, a pharetra augue. Cras justo odio, dapibus ac facilisis in, egestas eget quam. - [ ] ::(TASK):: Praesent commodo cursus magna, vel scelerisque nisl consectetur et. - [ ] ::(TASK):: Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. - ::(TASK NOTE):: Vivamus sagittis lacus vel augue laoreet rutrum faucibus dolor auctor. Cras justo odio, dapibus ac facilisis in, egestas eget quam. Maecenas faucibus mollis interdum. - ::(TASK NOTE):: Curabitur blandit tempus porttitor. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nulla vitae elit libero, a pharetra augue.

STEP 2: Extract the Coresponding Data for the Content

Use the Craft eXtension provided on this website for extracting the data source JSON of the Craft selection to the OmniFocus console.

This will provide the source material for developing your Omni Automation script for parsing the JSON and creating OmniFocus objects.

JSON Data of Craft Source Object


[ { "hasBlockDecoration": false, "listStyle": { "type": "toggle" }, "subblocks": [], "documentId": "2D8BE79D-A1C3-4935-9B85-70C4D63EA584", "content": [ { "isItalic": false, "isStrikethrough": false, "text": "(PROJECT TITLE)", "isCode": false, "highlightColor": "beachGradient", "isBold": false }, { "isItalic": false, "isStrikethrough": false, "text": " ", "isCode": false, "isBold": false }, { "isItalic": false, "isStrikethrough": false, "text": "IPSUM CURSUS PORTA", "isCode": false, "isBold": true } ], "hasFocusDecoration": false, "id": "E7A48736-B703-40D6-B870-59AB7D6191AE", "indentationLevel": 0, "style": { "fontStyle": "system", "alignmentStyle": "left", "textStyle": "body" }, "color": "text", "spaceId": "859c432e-33f3-7983-7c20-67a12bd618aa", "type": "textBlock" }, { "content": [ { "isItalic": false, "isStrikethrough": false, "text": "(PROJECT NOTE & LINK TO PROJECT)", "isCode": false, "highlightColor": "beachGradient", "isBold": false }, { "isItalic": false, "isStrikethrough": false, "text": " Aenean lacinia bibendum nulla sed consectetur. Vestibulum id ligula porta felis euismod semper. Aenean eu leo quam. Pellentesque ornare sem lacinia quam venenatis vestibulum.", "isCode": false, "isBold": false } ], "documentId": "2D8BE79D-A1C3-4935-9B85-70C4D63EA584", "id": "01E95014-0C55-4492-9F1B-B8BE693E52FA", "color": "text", "type": "textBlock", "subblocks": [], "hasBlockDecoration": false, "indentationLevel": 1, "spaceId": "859c432e-33f3-7983-7c20-67a12bd618aa", "style": { "fontStyle": "system", "alignmentStyle": "left", "textStyle": "body" }, "listStyle": { "type": "none" }, "hasFocusDecoration": true }, { "hasFocusDecoration": false, "spaceId": "859c432e-33f3-7983-7c20-67a12bd618aa", "color": "text", "content": [ { "isItalic": false, "isStrikethrough": false, "text": "(TASK)", "isCode": false, "highlightColor": "beachGradient", "isBold": false }, { "isItalic": false, "isStrikethrough": false, "text": " Nulla vitae elit libero, a pharetra augue. Cras justo odio, dapibus ac facilisis in, egestas eget quam.", "isCode": false, "isBold": false } ], "indentationLevel": 1, "subblocks": [], "type": "textBlock", "hasBlockDecoration": false, "documentId": "2D8BE79D-A1C3-4935-9B85-70C4D63EA584", "id": "082B56FE-9311-4B73-ADF4-33ED6475321E", "listStyle": { "type": "todo", "state": "unchecked" }, "style": { "fontStyle": "system", "alignmentStyle": "left", "textStyle": "body" } }, { "color": "text", "indentationLevel": 1, "style": { "fontStyle": "system", "alignmentStyle": "left", "textStyle": "body" }, "documentId": "2D8BE79D-A1C3-4935-9B85-70C4D63EA584", "hasFocusDecoration": false, "id": "FEF840E6-8B2D-415D-BF05-911B2B563A39", "content": [ { "isItalic": false, "isStrikethrough": false, "text": "(TASK)", "isCode": false, "highlightColor": "beachGradient", "isBold": false }, { "isItalic": false, "isStrikethrough": false, "text": " Praesent commodo cursus magna, vel scelerisque nisl consectetur et.", "isCode": false, "isBold": false } ], "hasBlockDecoration": false, "subblocks": [], "listStyle": { "type": "todo", "state": "unchecked" }, "type": "textBlock", "spaceId": "859c432e-33f3-7983-7c20-67a12bd618aa" }, { "content": [ { "isItalic": false, "isStrikethrough": false, "text": "(TASK)", "isCode": false, "highlightColor": "beachGradient", "isBold": false }, { "isItalic": false, "isStrikethrough": false, "text": " Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus.", "isCode": false, "isBold": false } ], "indentationLevel": 1, "type": "textBlock", "color": "text", "hasBlockDecoration": false, "documentId": "2D8BE79D-A1C3-4935-9B85-70C4D63EA584", "id": "04EC3664-CF69-4722-8FD8-BDF48D48B0A3", "spaceId": "859c432e-33f3-7983-7c20-67a12bd618aa", "hasFocusDecoration": false, "listStyle": { "type": "todo", "state": "unchecked" }, "style": { "fontStyle": "system", "alignmentStyle": "left", "textStyle": "body" }, "subblocks": [] }, { "type": "textBlock", "hasBlockDecoration": false, "indentationLevel": 2, "documentId": "2D8BE79D-A1C3-4935-9B85-70C4D63EA584", "hasFocusDecoration": false, "color": "text", "id": "B4F91B09-67C3-4AD8-BF69-74D010CC0489", "subblocks": [], "style": { "fontStyle": "system", "alignmentStyle": "left", "textStyle": "body" }, "spaceId": "859c432e-33f3-7983-7c20-67a12bd618aa", "content": [ { "isItalic": false, "isStrikethrough": false, "text": "(TASK NOTE)", "isCode": false, "highlightColor": "beachGradient", "isBold": false }, { "isItalic": false, "isStrikethrough": false, "text": " Vivamus sagittis lacus vel augue laoreet rutrum faucibus dolor auctor. Cras justo odio, dapibus ac facilisis in, egestas eget quam. Maecenas faucibus mollis interdum.", "isCode": false, "isBold": false } ], "listStyle": { "type": "bullet" } }, { "listStyle": { "type": "bullet" }, "hasFocusDecoration": false, "hasBlockDecoration": false, "subblocks": [], "documentId": "2D8BE79D-A1C3-4935-9B85-70C4D63EA584", "color": "text", "style": { "fontStyle": "system", "alignmentStyle": "left", "textStyle": "body" }, "type": "textBlock", "id": "E9C9ACB7-1EFE-42AA-8FD9-9510A6EF2AF5", "spaceId": "859c432e-33f3-7983-7c20-67a12bd618aa", "indentationLevel": 2, "content": [ { "isItalic": false, "isStrikethrough": false, "text": "(TASK NOTE)", "isCode": false, "highlightColor": "beachGradient", "isBold": false }, { "isItalic": false, "isStrikethrough": false, "text": " Curabitur blandit tempus porttitor. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nulla vitae elit libero, a pharetra augue.", "isCode": false, "isBold": false } ] } ]

STEP 3: Code Wrapper for Testing

To make it easier to use Omni Automation forms in your script, wrap your development code within this self-invoking asynchronous wrapper. This will enable your script to prompt the user for input if needed by the script.

Code Wrapper


(async () => { // PLACE SCRIPT CODE WITHIN ASCYHRONOUS WRAPPER })();

STEP 4: Error Handler

To assist you in uncovering errors in your coding, add a global error handler within the outer code wrapper. Any error messages will be displayed in OmniFocus when you execute the script from the OmniFocus console.

Error Handler


(async () => { try { // PARSING/CREATION CODE GOES HERE } catch(err){ if(!err.message.includes("cancelled")){ await new Alert(err.name, err.message).show() } throw `${err.name}\n${err.message}` } })();

STEP 5: Enter Craft Data into OmniFocus Console

With the example selected in the Craft document, run the provided Craft data extraction eXtension. Copy the passed Craft data JSON now showing in the OmniFocus console, and then enter the extracted data again into Console using this simple variable declaration:

Declare the Passed Craft Data


var passedData = PASTE COPIED CRAFT DATA HERE

(⬇ see below ) The console after declaring the passed data:

Delcaring the passed data in the console

STEP 6: Iterating the Passed Data Objects

Once the data has been declared, it can be referenced in your code as you develop the script in the OmniFocus console.

The following script demonstrates the iteration of the passed data objects:

Parse Data


(async () => { try { passedData.forEach((block, index) => { blockType = block.type listStyleType = block.listStyle.type console.log(index, block.type, listStyleType) }) } catch(err){ if(!err.message.includes("cancelled")){ await new Alert(err.name, err.message).show() } throw `${err.name}\n${err.message}` } })();

(⬇ see below ) Iterating the passed data objects to retrieve property values:

Iterated passed data in console

STEP 7: Create the Project

Evolving the example script, here is code that creates a new OmniFocus project with a back-link to the first block in the Craft selection:

Create the Project in OmniFocus


(async () => { try { passedData.forEach((block, index) => { blockType = block.type listStyleType = block.listStyle.type console.log(index, block.type, listStyleType) if(index === 0){ if (blockType !== "textBlock" || listStyleType !== "toggle"){ throw { name: "Block Issue", message: "The first block must be a text block with a toggle." } } segments = new Array() block.content.forEach(segment => segments.push(segment.text)) projectTitle = segments.join("") var project = new Project(projectTitle) blockID = block.id spaceID = block.spaceId backLink = `craftdocs://open?blockId=${blockID}&spaceId=${spaceID}` project.note = backLink } }) id = project.id.primaryKey URL.fromString(`omnifocus:///task/${id}`).open() } catch(err){ if(!err.message.includes("cancelled")){ await new Alert(err.name, err.message).show() } throw `${err.name}\n${err.message}` } })();

(⬇ see below ) The new project in OmniFocus with a back-link to the first Craft block of the selected items.

The new project in OmniFocus

Step 8: Add Other Components

Next, add the code for adding tasks and their related notes (if any). NOTE: tasks are identified by having a list type of: todo

Complete Parsing/Create Script


(async () => { var project var latestTask try { passedData.forEach((block, index) => { var blockType = block.type var listStyleType = block.listStyle.type console.log(index, block.type, listStyleType) if(index === 0){ // CREATE PROJECT WITH BACK-LINK if (blockType !== "textBlock" || listStyleType !== "toggle"){ throw { name: "Block Issue", message: "The first block must be a text block with a toggle." } } segments = new Array() block.content.forEach(segment => segments.push(segment.text)) projectTitle = segments.join("") project = new Project(projectTitle) blockID = block.id spaceID = block.spaceId backLink = `craftdocs://open?blockId=${blockID}&spaceId=${spaceID}` project.note = backLink } if(index === 1 && blockType === "textBlock" && listStyleType === "none" ){ // ADD NOTES TO PROJECT segments = new Array() block.content.forEach(segment => segments.push(segment.text)) projectNotes = segments.join("") project.note = project.note + "\n\n" + projectNotes } if (index > 1 && blockType === "textBlock" && listStyleType === "todo"){ // ADD TASK TO PROJECT segments = new Array() block.content.forEach(segment => segments.push(segment.text)) taskTitle = segments.join("") latestTask = new Task(taskTitle, project) } if (index > 1 && blockType === "textBlock" && listStyleType === "bullet"){ // ADD BULLETED NOTE TO PREVIOUS TASK segments = new Array() block.content.forEach(segment => segments.push(segment.text)) taskNote = segments.join("") bulletChar = String.fromCharCode(8226) if (!latestTask){targetObject = project} else {targetObject = latestTask} if (targetObject.note.length === 0){ targetObject.appendStringToNote(bulletChar + " " + taskNote) } else { targetObject.appendStringToNote("\n" + bulletChar + " " + taskNote) } } if (index > 1 && blockType === "textBlock" && listStyleType === "none"){ // ADD NOTE TO PREVIOUS TASK segments = new Array() block.content.forEach(segment => segments.push(segment.text)) taskNote = segments.join("") if (!latestTask){targetObject = project} else {targetObject = latestTask} if (targetObject.note.length === 0){ targetObject.appendStringToNote(taskNote) } else { targetObject.appendStringToNote("\n" + taskNote) } } }) } catch(err){ if(!err.message.includes("cancelled")){ await new Alert(err.name, err.message).show() } throw `${err.name}\n${err.message}` } })();

(⬇ see below ) The new project in OmniFocus recreated from the Craft data.

The transferred project

The HTML File

The Craft eXtension bundle includes an HTML file that contains (at a minimum):

The HTML File


<!DOCTYPE html> <html> <head> <script> function goToDocumentation(){ craft.editorApi.openURL("omni-automation.com/craft/omnifocus-new-item.html") } </script> <style> body { font-family: -apple-system, Helvetica, sans-serif; font-size: 10pt; } h2 { font-family: -apple-system, Helvetica, sans-serif; font-size: 12pt; } #main-script-btn { font-family: -apple-system, Helvetica, sans-serif; font-size: 12pt; background-color: lightBlue; text-transform: uppercase; padding: 4px; } #documentation-link { font-size:80%; text-decoration: none; } </style> </head> <body> <!-- EXTENSION INTERFACE --> <button id="main-script-btn">Recreate in OmniFocus</button> <h2>Recreate Project in OmniFocus</h2> <p>This Craft extension will recreate the selected project items in a new project in OmniFocus.</p> <p><a id="documentation-link" href="javascript:void(0);" onclick="goToDocumentation();">Documentation</a></p> <!-- MAIN SCRIPT --> <script> runButton = document.getElementById("main-script-btn"); runButton.addEventListener("click", async () => { // GET THE CURRENT SELECTION result = await craft.editorApi.getSelection() if (result.status !== "success") { throw new Error(result.message) } // FUNCTION TO BE EXECUTED BY OMNIFOCUS function transferContentToOmniFocus(passedData){ (async () => { var project var latestTask try { passedData.forEach((block, index) => { var blockType = block.type var listStyleType = block.listStyle.type console.log(index, block.type, listStyleType) if(index === 0){ // CREATE PROJECT WITH BACK-LINK if (blockType !== "textBlock" || listStyleType !== "toggle"){ throw { name: "Block Issue", message: "The first block must be a text block with a toggle." } } segments = new Array() block.content.forEach(segement => segments.push(segement.text)) projectTitle = segments.join("") project = new Project(projectTitle) blockID = block.id spaceID = block.spaceId backLink = `craftdocs://open?blockId=${blockID}&spaceId=${spaceID}` project.note = backLink } if(index === 1 && blockType === "textBlock" && listStyleType === "none" ){ // ADD NOTES TO PROJECT segments = new Array() block.content.forEach(segement => segments.push(segement.text)) projectNotes = segments.join("") project.note = project.note + "\n\n" + projectNotes } if (index > 1 && blockType === "textBlock" && listStyleType === "todo"){ // ADD TASK TO PROJECT segments = new Array() block.content.forEach(segement => segments.push(segement.text)) taskTitle = segments.join("") latestTask = new Task(taskTitle, project) } if (index > 1 && blockType === "textBlock" && listStyleType === "bullet"){ // ADD BULLETED NOTE TO PREVIOUS TASK segments = new Array() block.content.forEach(segement => segments.push(segement.text)) taskNote = segments.join("") bulletChar = String.fromCharCode(8226) if (!latestTask){targetObject = project} else {targetObject = latestTask} if (targetObject.note.length === 0){ targetObject.appendStringToNote(bulletChar + " " + taskNote) } else { targetObject.appendStringToNote("\n" + bulletChar + " " + taskNote) } } if (index > 1 && blockType === "textBlock" && listStyleType === "none"){ // ADD NOTE TO PREVIOUS TASK segments = new Array() block.content.forEach(segement => segments.push(segement.text)) taskNote = segments.join("") if (!latestTask){targetObject = project} else {targetObject = latestTask} if (targetObject.note.length === 0){ targetObject.appendStringToNote(taskNote) } else { targetObject.appendStringToNote("\n" + taskNote) } } }) } catch(err){ if(!err.message.includes("cancelled")){ await new Alert(err.name, err.message).show() } throw `${err.name}\n${err.message}` } })(); } // CREATE FUNCTION AND CONTENT STRINGS var contentString = JSON.stringify(result.data) var encodedContent = encodeURIComponent(contentString) var functionString = transferContentToOmniFocus.toString() var encodedFunction = encodeURIComponent(functionString) // CREATE SCRIPT URL url = 'omnifocus://localhost/omnijs-run?script=' + '%28' + encodedFunction + '%29' + '%28' + 'argument' + '%29' + '&arg=' + encodedContent // EXECUTE THE URL await craft.editorApi.openURL(url) }); </script> </body> </html>

The Manifest JSON File

The manifest.json file contains the metadata for the Craft eXtension:

Manifest JSON


{ "id": "omni-automation-cr-of-recreate-structured-content", "name": "Recreate Structured Content", "fileName": "recreate-structured-content-in-omnifocus", "author": "Otto Automator", "author-email": "omni_automation@icloud.com", "description" : "Recreates the structured Craft selection in OmniFocus as a project.", "api": "0.1.0", "main": "index.html" }

Creating eXtension Bundle

The following command is executed in the Terminal app to convert the folder containing the eXtension components into a specialized ZIP archive:

Terminal Command


cd /path-to-the-folder-containing-the-files zip -vr recreate-structured-content-in-omnifocus.craftx icon.png index.html manifest.json