OO-PG0017
Plug-In: Export Outline to JSON File
Exports the contents of the outline to a file in JSON format.
The source outline can contain multiple custom columns and notes, and a cell (Row/Column intersecton) value can be a Number, Text, Date, Checkbox, Enumeration, or Duration. Durations can be expressed in either their short-form text versions: 3d 2.25h or verbose forms, while the value of checkboxes can be expressed as either the integers or terms: 0 = unchecked, 1 = checked, and 2 = mixed.
The contents of the outline will be exported row-by-row from the top-down, and will include a numeric hierarchy level index for each row.
Some of the export parameters can be changed by adjusting the plug-in’s Preferences Dialog (see image). To access this plug-in’s preferences dialog, hold down the Shift key when selecting the plug-in from the Automation Menu.

Return to: OmniOutliner Plug-In Collection
Export Outline to JSON File
/*{
"type": "action",
"targets": ["omnioutliner"],
"author": "Otto Automator",
"identifier": "com.omni-automation.oo.export-to-json-file",
"version": "1.1",
"description": "Exports the contents of the outline to a file in JSON format. Outline can contain multiple custom columns and notes. A cell (Row/Column intersecton) value can be a Number, Text, Date, Checkbox, Enumeration, or Duration. Durations can be expressed in either their short-form text versions: 3d 2.25h or verbose forms, while the value of checkboxes can be expressed as either the integers or terms: 0 = unchecked, 1 = checked, and 2 = mixed. The contents of the outline will be exported row-by-row from the top-down, and will include a numeric hierarchy level parameter for each row. To access this plug-in’s preferences dialog, hold down the Shift key when selecting the plug-in from the Automation Menu.",
"label": "Export to JSON File",
"shortLabel": "JSON Export",
"paletteLabel": "JSON Export",
"image": "square.and.arrow.up"
}*/
(() => {
function storeDataInAppFolder(dataObj, fileName, endingActionIndicator){
try {
dataObjStr = JSON.stringify(dataObj)
data = Data.fromString(dataObjStr)
folderURL = URL.documentsDirectory
fileURL = folderURL.appendingPathComponent(fileName)
wrapper = FileWrapper.withContents(fileName, data)
wrapper.write(fileURL, [FileWrapper.WritingOptions.Atomic], null)
if(endingActionIndicator !== 0 && app.platformName === "macOS"){
if (endingActionIndicator === 1){
fileURL.open()
} else {
folderURL.open()
}
}
}
catch(err){
new Alert(err.name, err.message).show()
}
}
function logToConsole(title, data){
if(enableLogging){
console.log(title, data)
}
}
var preferences = new Preferences() // NO ID = PLUG-IN ID
var enableLogging
const action = new PlugIn.Action(async function(selection, sender){
try {
// PLUG-IN PREFERENCES
exportDurationsInLongForm = preferences.readBoolean("exportDurationsInLongForm")
if(!exportDurationsInLongForm instanceof Boolean){
exportDurationsInLongForm = false
// 3 days 2.5 hours vs. 3d 2.25h
}
logToConsole("STATE: exportDurationsInLongForm", exportDurationsInLongForm)
exportCheckboxStateNumerically = preferences.readBoolean("exportCheckboxStateNumerically")
if(!exportCheckboxStateNumerically instanceof Boolean){
exportCheckboxStateNumerically = false
// 0 = "unchecked", 1 = "checked", 2 = "mixed"
}
logToConsole("STATE: exportCheckboxStateNumerically", exportCheckboxStateNumerically)
exportUsingDocumentName = preferences.readBoolean("exportUsingDocumentName")
if(!exportUsingDocumentName instanceof Boolean){
exportUsingDocumentName = false
// name the export document the outline name with “.json” file extension
}
logToConsole("STATE: exportUsingDocumentName", exportUsingDocumentName)
exportToDocumentsFolder = preferences.readBoolean("exportToDocumentsFolder")
if(!exportToDocumentsFolder instanceof Boolean){
exportToDocumentsFolder = true
// automatically export to local OmniOutliner Documents
}
logToConsole("STATE: exportToDocumentsFolder", exportToDocumentsFolder)
openExportedFile = preferences.readBoolean("openExportedFile")
if(!openExportedFile instanceof Boolean){
openExportedFile = true
// (macOS) open the exported file in the default JSON Editor app
}
logToConsole("STATE: openExportedFile", openExportedFile)
openExportDirectory = preferences.readBoolean("openExportDirectory")
if(!openExportDirectory instanceof Boolean){
openExportDirectory = false
// (macOS) open the folder containing the exported file
}
logToConsole("STATE: openExportDirectory", openExportDirectory)
enableLogging = preferences.readBoolean("enableLogging")
if(!enableLogging instanceof Boolean){
enableLogging = true
// log the plug-in’s actions and results
}
logToConsole("STATE: enableLogging", enableLogging)
if(app.shiftKeyDown){
durationsCheckSwitchField = new Form.Field.Checkbox(
"exportDurationsInLongForm",
"Export duration values in verbose form",
exportDurationsInLongForm
)
checkboxesCheckSwitchField = new Form.Field.Checkbox(
"exportCheckboxStateNumerically",
"Export checkbox states as integers",
exportCheckboxStateNumerically
)
filenameCheckSwitchField = new Form.Field.Checkbox(
"exportUsingDocumentName",
"Name export file using outline title",
exportUsingDocumentName
)
destinationCheckSwitchField = new Form.Field.Checkbox(
"exportToDocumentsFolder",
"Export file to local OmniOutliner Documents folder",
exportToDocumentsFolder
)
openFileCheckSwitchField = new Form.Field.Checkbox(
"openExportedFile",
"(macOS) Open exported JSON file in default editor application",
openExportedFile
)
openFolderCheckSwitchField = new Form.Field.Checkbox(
"openExportDirectory",
"(macOS) Open folder containing the exported JSON file",
openExportDirectory
)
loggingCheckSwitchField = new Form.Field.Checkbox(
"enableLogging",
"Log the plug-in’s actions and results to the OmniOutliner Console",
enableLogging
)
inputForm = new Form()
inputForm.addField(durationsCheckSwitchField)
inputForm.addField(checkboxesCheckSwitchField)
inputForm.addField(filenameCheckSwitchField)
inputForm.addField(destinationCheckSwitchField)
inputForm.addField(openFileCheckSwitchField)
inputForm.addField(openFolderCheckSwitchField)
inputForm.addField(loggingCheckSwitchField)
formObject = await inputForm.show("Plug-in Preferences:","Continue")
exportDurationsInLongForm = formObject.values['exportDurationsInLongForm']
exportCheckboxStateNumerically = formObject.values['exportCheckboxStateNumerically']
exportUsingDocumentName = formObject.values['exportUsingDocumentName']
exportToDocumentsFolder = formObject.values['exportToDocumentsFolder']
openExportedFile = formObject.values['openExportedFile']
openExportDirectory = formObject.values['openExportDirectory']
enableLogging = formObject.values['enableLogging']
preferences.write("exportDurationsInLongForm", exportDurationsInLongForm)
preferences.write("exportCheckboxStateNumerically", exportCheckboxStateNumerically)
preferences.write("exportUsingDocumentName", exportUsingDocumentName)
preferences.write("exportToDocumentsFolder", exportToDocumentsFolder)
preferences.write("openExportedFile", openExportedFile)
preferences.write("openExportDirectory", openExportDirectory)
preferences.write("enableLogging", enableLogging)
logToConsole("NEW: exportDurationsInLongForm", exportDurationsInLongForm)
logToConsole("NEW: exportCheckboxStateNumerically", exportCheckboxStateNumerically)
logToConsole("NEW: exportUsingDocumentName", exportUsingDocumentName)
logToConsole("NEW: exportToDocumentsFolder", exportToDocumentsFolder)
logToConsole("NEW: openExportedFile", openExportedFile)
logToConsole("NEW: openExportDirectory", openExportDirectory)
logToConsole("NEW: enableLogging", enableLogging)
return "plug-in preferences set"
}
// DECLARE TOP LEVEL OBJECTS: EDITOR, OUTLINE (TREE)
editor = document.editors[0]
tree = document.outline
// IS STATUS COLUMN VISIBLE?
checkboxVisible = editor.visibilityOfColumn(tree.statusColumn)
// CREATE FORMATTERS AS NEEDED
for (column of columns){
if(column.type === Column.Type.Duration){
logToConsole("NOTE: ", "CREATING DURATION FORMATTER")
var durationFmtr = new Formatter.Duration()
durationFmtr.useVerboseFormat = exportDurationsInLongForm
break
}
}
// BEGIN EXPORT
logToConsole("NOTE: ", "BEGINNING EXPORT")
exportObj = new Array()
for ([index, aRow] of rootItem.descendants.entries()){
logToConsole("INCREMENT: ", "ROW " + index)
// GET THE CORREPSONDING NODE FOR THE ROW
aNode = editor.nodeForObject(aRow)
// CREATE STORAGE CONTAINER FOR ROW DATA
itemObj = new Object()
// ADD ROW LEVEL INFO
itemObj.level = aRow.level
// PROCESS EACH COLUMN OF THE ROW
for ([index, aColumn] of columns.entries()){
logToConsole("COLUMN INDEX", index)
if(checkboxVisible && aColumn === statusColumn){
logToConsole("NOTE: ", "STATUS COLUMN")
state = aNode.state
if(state === State.Checked){
stateValue = (exportCheckboxStateNumerically === true) ? 1:"checked"
itemObj.status = stateValue
} else if (state === State.Mixed){
stateValue = (exportCheckboxStateNumerically === true) ? 2:"mixed"
itemObj.status = stateValue
} else if (state === State.Unchecked){
stateValue = (exportCheckboxStateNumerically === true) ? 0:"unchecked"
itemObj.status = stateValue
}
} else if(aColumn === noteColumn){
logToConsole("NOTE: ", "NOTE COLUMN")
columnTitle = aColumn.title
if(columnTitle === ""){
itemObj.note = aRow.note
} else {
itemObj[columnTitle] = aRow.note
}
} else if(aColumn === outlineColumn){
logToConsole("NOTE: ", "TOPIC COLUMN")
columnTitle = aColumn.title
if(columnTitle === ""){
itemObj.topic = aRow.topic
} else {
itemObj[columnTitle] = aRow.topic
}
} else {
logToConsole("NOTE: ", "CUSTOM COLUMN")
columnTitle = aColumn.title
if(columnTitle === ""){
columnTitle = `COL-${index}`
}
columnType = aColumn.type
// [ CHECKBOX, DATE, DURATION, ENUMERATION, NUMBER, TEXT ]
// GET VALUE OF COLUMN/ROW CELL
cellValue = aRow.valueForColumn(aColumn)
logToConsole("CELL VALUE", cellValue)
// ADD TO DATA OBJECT
switch (columnType) {
case Column.Type.Checkbox:
logToConsole("NOTE: ", "COLUMN TYPE: CHECKBOX")
state = aNode.state
if(state === State.Checked){
stateValue = (exportCheckboxStateNumerically === true) ? 1:"checked"
} else if (state === State.Mixed){
stateValue = (exportCheckboxStateNumerically === true) ? 2:"mixed"
} else if (state === State.Unchecked){
stateValue = (exportCheckboxStateNumerically === true) ? 0:"unchecked"
}
itemObj[columnTitle] = stateValue
break;
case Column.Type.Date:
logToConsole("NOTE: ", "COLUMN TYPE: DATE")
cellValue = cellValue.toISOString()
itemObj[columnTitle] = cellValue
break;
case Column.Type.Duration:
logToConsole("NOTE: ", "COLUMN TYPE: DURATION")
cellValue = durationFmtr.stringFromDecimal(cellValue)
itemObj[columnTitle] = cellValue
break;
case Column.Type.Enumeration:
logToConsole("NOTE: ", "COLUMN TYPE: ENUMERATION")
cellValue = cellValue.name
itemObj[columnTitle] = cellValue
break;
case Column.Type.Number:
logToConsole("NOTE: ", "COLUMN TYPE: NUMBER")
itemObj[columnTitle] = cellValue
break;
case Column.Type.Text:
logToConsole("NOTE: ", "COLUMN TYPE: TEXT")
// GET STRING OF TEXT OBJECT
cellValue = cellValue.string
itemObj[columnTitle] = cellValue
break;
default:
logToConsole("NOTE: ", "COLUMN TYPE: UNKNOWN")
cellValue = cellValue.toString()
itemObj[columnTitle] = cellValue
}
}
}
logToConsole("ROW OBJECT: ", JSON.stringify(itemObj))
exportObj.push(itemObj)
}
logToConsole("OUTLINE: ", JSON.stringify(exportObj, null, 2))
documentName = document.name.replace(/\.[^/.]+$/, "")
logToConsole("DOCUMENT NAME:", documentName)
exportFileName = documentName + ".json"
if(exportToDocumentsFolder === true){
if(exportUsingDocumentName == false){
textInputField = new Form.Field.String(
"textInput",
null,
exportFileName
)
inputForm = new Form()
inputForm.addField(textInputField)
inputForm.validate = function(formObject){
inputText = formObject.values['textInput']
return ((!inputText)?false:true)
}
formPrompt = "Name for export file:"
buttonTitle = "Continue"
formObject = await inputForm.show(formPrompt, buttonTitle)
exportFileName = formObject.values['textInput']
if(!exportFileName.endsWith(".json")){
exportFileName = exportFileName + ".json"
}
logToConsole('exportFileName: ',exportFileName)
}
endingActionIndicator = 0
if (openExportedFile){endingActionIndicator = 1}
if (openExportDirectory){endingActionIndicator = 2}
storeDataInAppFolder(exportObj, exportFileName, endingActionIndicator)
} else {
data = Data.fromString(JSON.stringify(exportObj))
wrapper = FileWrapper.withContents(exportFileName, data)
filesaver = new FileSaver()
urlObj = await filesaver.show(wrapper)
logToConsole("FILE:", urlObj.path)
if(openExportedFile){
urlObj.open()
} else if (openExportDirectory){
parentDirectoryURL = urlObj.deletingLastPathComponent( )
parentDirectoryURL.open()
}
}
}
catch(err){
if(!causedByUserCancelling){
new Alert(err.name, err.message).show()
}
}
});
action.validate = function(selection, sender){
return true
};
return action;
})();