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. |
|
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:
- Be consistent in its design, with a logic making its structural components easy to parse.
- Have elements that be recreated as corresponding elements in OmniFocus.
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.
These logical assumptions are made for the example Craft object:
- The initial block will represent the OmniFocus project. It must have a type of textBlock and have a listStyle.type of toggle
- The next block will represent the note for OmniFocus project. It must have a type of textBlock. You can include as many paragraphs of notes as needed.
- Blocks that follow the project note block that have a type of textBlock AND have a listStyle.type of todo will become tasks of the project.
- Blocks that follow the project note block that have a type of textBlock AND do not have a listStyle.type of todo will become notes of the previous task. If the blocks have a listStyle.type of bullet will have a bullet character precede the task note.
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
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}`
}
})();
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}`
}
})();
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}`
}
})();
The HTML File
The Craft eXtension bundle includes an HTML file that contains (at a minimum):
- An interface element to trigger the execution of the main script (line 33)
- The main script code to be executed (lines 40-144). This code retrieves the Craft selection data, and then creates and executes an Omni Automation script URL targeting the OmniFocus application.
- The wrapped code for creating the OmniFocus element, is placed within a function that will be used in an Omni Automation script URL for both transferring the Craft selection data and for using the data to create an new OmniFocus project (lines 53-125)
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