content: -- Copyright (C) 2012 Robin Trew
  --
  -- Permission is hereby granted, free of charge, 
  -- to any person obtaining a copy of this software 
  -- and associated documentation files (the "Software"), 
  -- to deal in the Software without restriction, 
  -- including without limitation the rights to use, copy, 
  -- modify, merge, publish, distribute, sublicense, 
  -- and/or sell copies of the Software, and to permit persons 
  -- to whom the Software is furnished to do so, 
  -- subject to the following conditions:
  
  -- *******
  -- The above copyright notice and this permission notice 
  -- shall be included in ALL copies 
  -- or substantial portions of the Software.
  -- *******
  
  -- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 
  -- EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 
  -- OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 
  -- IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 
  -- DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 
  -- TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE 
  -- OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
  
  property pTitle : "OmniOutliner3: Copy/Save As FoldingText"
  property pVer : "0.21"
  property pAuthor : "Robin Trew"
  property pSite : "Originally published on http://forums.omnigroup.com"
  
  -- COPIES SELECTED OO3 OUTLINE (WITH ANY COLUMN FIELDS) IN FOLDINGTEXT MARKDOWN FORMAT
  -- COLUMN VALUES OF OVER TWO WORDS BECOME COLUMN_NAME : VALUE PROPERTY LINES IN FT
  -- AND SHORTER COLUMN VALUES BECOME IN LINE @COLUMN_NAME(VALE) TAGS IN FT
  
  -- COPY ONLY THE SELECTED OMNIOUTLINER 3 NODES AND THEIR SUBTREES ?
  property pblnSeln : true
  
  -- EXPORT ANY DATA IN USER COLUMNS TO FOLDINGTEXT @KEY(VALUE) TAGS OR KEY : VALUE PROPERTIES ?
  property pblnUserColumns : true
  property plngTagWordsMax : 2 -- threshold at which @key(value) inline tags yield to Key : Value child lines 
  
  -- LOOK FOR **BOLD** OR *ITALIC/OBLIQUE* FONTS IN OO3 AND TRANSLATE THEM INTO
  -- MARKDOWN EMPHASES ?
  property pblnTopicEmphasis : true
  property pblnNoteEmphasis : true
  
  -- PLACE THE FOLDINGTEXT VERSION IN THE CLIPBOARD ?
  property pblnSetClipboard : true
  
  -- SAVE TO A FILE ?
  property pblnWriteFile : false
  
  -- NOTIFY ON SAVE/COPY ?
  property pblnNotify : true -- With Growl, if installed, or with AS dialog
  
  property plngMinHash : 1 -- Number of hashes for the highest level exported headers
  property plngMaxHash : 2 -- Deepest level of OO3 nesting to flag as headers rather than bullets etc
  property pOutFolder : (path to desktop) -- set this to "" to default to the .oo3 file's folder
  property pstrExtn : "ft" -- Obvious choices are ft or txt
  -- To preserve oo3's nesting, multi-line 003 nodes are coerced to one line
  property pstrLineDelim : " – " -- This is the string used to delimit the single line.
  -- (oo3 notes, however, are exported as a series of paragraphs).
  
  property piHeader : 1
  property piUnordered : 2
  property piOrdered : 3
  property piQuote : 4
  property piCode : 5
  property piBody : 6
  property plstTypes : {"Header", "Bulleted", "Numbered", "Quote", "Code", "Body"}
  property plstPrefixes : {"", "- ", "1. ", "> ", "    ", ""}
  property pstrDefaultPrefix : "- "
  
  property pAttribNumbered : "heading-type(com.omnigroup.OmniOutliner)"
  property pstrNoNumber : "None"
  property pstrDone : "@done"
  property pstrTypeCol : "ft_type" -- Optional popup column in .oo3 offering node type choice (plstTypes)
  
  on run
  	tell application id "OOut"
  		-- GET A DOCUMENT OR EXIT
  		set lstDocs to documents
  		if length of lstDocs < 1 then return
  		tell item 1 of lstDocs
  			
  			-- GET A NESTED LIST REPRESENTATION OF THE .OO3 DOCUMENT
  			set lstDoc to my OODoc2Lst(it, pblnSeln)
  			if pblnWriteFile then
  				set {strOOName, strOOPath} to {name, path}
  				try
  					strOOPath
  				on error
  					display dialog "OmniOutliner file not yet saved: " & return & return & tab & strOOName & return & return & ¬
  						"Save .oo3 file before exporting to FoldingText." buttons {"OK"} default button "OK" with title pTitle & "  ver. " & pVer
  					return
  				end try
  			end if
  		end tell
  	end tell
  	
  	-- TRANSLATE THE NESTED LIST TO A FOLDINGTEXT STRING
  	-- (Document, Nesting level, Hash depth)
  	set blnHashes to (plngMinHash) > 0
  	if plngMaxHash < plngMinHash then set plngMaxHash to plngMinHash
  	if blnHashes then
  		set lngHashDepth to plngMinHash
  	else
  		set lngHashDepth to -1
  	end if
  	set strFT to MakeFT(lstDoc, plngMinHash, lngHashDepth, blnHashes)
  	if pblnSetClipboard then
  		set the clipboard to strFT
  		if pblnNotify then Notify("OmniOutliner Professional", "", "Copied", strFT)
  	end if
  	
  	-- OFFER TO WRITE EITHER TO A DEFAULT FOLDER, 
  	-- OR TO THE FOLDER OF THE .OO3 FILE
  	if pblnWriteFile then
  		set strPath to ChooseFilePathAndSave(strFT, strOOPath, strOOName)
  		if pblnNotify then Notify("OmniOutliner Professional", "", "Saved", strPath)
  	end if
  	-- return strFT
  end run
  
  -- TRANSLATE NESTED APPLESCRIPT LIST TO FT STRING
  -- USING HASH HEADERS FOR THE FIRST N LEVELS
  on MakeFT(lstDoc, lngNestLevel, lngHashDepth, blnHashes)
  	set strFT to ""
  	-- LEVEL OF ANY TAB OR HASH NESTING
  	set strTabs to stringof(lngNestLevel - (lngHashDepth + 1), tab)
  	set strHashes to stringof(lngHashDepth, "#")
  	
  	-- GET THE TYPE NAMES BEING USED
  	set strHeader to item piHeader of plstTypes
  	set strOrdered to item piOrdered of plstTypes
  	
  	-- WILL CHILDREN OF THIS NODE HAVE HASH PREFIXES ?
  	set blnHashDescendants to blnHashes and (lngHashDepth < plngMaxHash)
  	
  	repeat with oRow in lstDoc
  		set {strTopic, lstNote, blnDone, strType, lngIndex, lstTags, lstChiln} to oRow
  		-- PREFIX THE TEXT ACCORDING TO THE FT NODE TYPE
  		set blnHashCancelled to false
  		if (lngIndex > 0) or (strType ≠ "") then
  			if strType = strHeader then
  				set strPrefix to strHashes & space
  			else
  				blnHashDescendants = false
  				-- Make a note if a hash expected at this level was overriden by a special node type (ordered, etc)
  				if blnHashes then set blnHashCancelled to true
  				set strTypePrefix to GetPrefix(strType, lngIndex)
  				set strPrefix to strTabs & strTypePrefix
  			end if
  		else
  			if blnHashes then
  				set strPrefix to strHashes & space
  			else
  				set strPrefix to strTabs & pstrDefaultPrefix
  			end if
  		end if
  		set strFT to strFT & strPrefix & strTopic
  		
  		-- WRITE OUT ANY OO3 COLUMN VALUES AS FT TAGS (IF REQUIRED)
  		set lstProps to {}
  		if pblnUserColumns then
  			repeat with oTag in lstTags
  				set {strKey, varVal} to oTag
  				set lngWords to 0
  				if varVal is not true then set lngWords to length of (words of varVal)
  				
  				if (lngWords > plngTagWordsMax) then
  					set end of lstProps to {strKey, varVal}
  				else
  					set strFT to strFT & space & "@" & strKey
  					if lngWords > 0 then set strFT to strFT & "(" & varVal & ")"
  				end if
  			end repeat
  		end if
  		
  		if blnDone then set strFT to strFT & space & pstrDone
  		
  		-- AND END THE LINE
  		set strFT to strFT & linefeed
  		
  		-- ADD ANY PROPERTY LINES (indented by one additional tab)
  		if lstProps ≠ {} then
  			if strPrefix contains "#" then
  				set strPropPrefix to tab
  			else
  				set strPropPrefix to strTabs & tab
  			end if
  			repeat with oProp in lstProps
  				set {strKey, strValue} to oProp
  				set strFT to strFT & strPropPrefix & strKey & " : " & strValue & linefeed
  			end repeat
  		end if
  		
  		-- ADD ANY NOTE PARAGRAPHS (indented by one additional tab and prefix required)
  		if lstNote ≠ {} then
  			if strPrefix contains "#" then
  				set strNotePrefix to tab
  			else
  				set strNotePrefix to strTabs & tab
  				if strType is in {item piQuote of plstTypes, item piCode of plstTypes} then ¬
  					set strNotePrefix to strNotePrefix & strTypePrefix
  			end if
  			repeat with oPara in lstNote
  				set strFT to strFT & strNotePrefix & oPara
  			end repeat
  			set strFT to strFT & linefeed
  		end if
  		
  		-- AND RECURSE WITH ANY CHILDREN TO THE NEXT LEVEL OF NESTING
  		if blnHashCancelled then set blnHashDescendants to false
  		if length of lstChiln > 0 then
  			if blnHashDescendants then
  				set lngNextHash to lngHashDepth + 1
  			else
  				if blnHashes and blnHashCancelled then
  					set lngNextHash to lngHashDepth - 1
  				else
  					set lngNextHash to lngHashDepth
  				end if
  			end if
  			set strFT to strFT & MakeFT(lstChiln, lngNestLevel + 1, lngNextHash, blnHashDescendants)
  		end if
  	end repeat
  	return strFT
  end MakeFT
  
  -- SAVE THE FT STRING TO A TEXT FILE
  on ChooseFilePathAndSave(strFT, strOOPath, strOOName)
  	tell application "Finder"
  		-- OFFER A DEFAULT FOLDER (if a valid one is specified)
  		if exists pOutFolder then
  			set strOutFolder to POSIX path of pOutFolder
  		else
  			-- OR OFFER THE FOLDER CONTAINING THE .OO3 FILE
  			set lngName to length of strOOName
  			set strOutFolder to text 1 thru ((-lngName) - 1) of strOOPath
  		end if
  	end tell
  	set {dlm, my text item delimiters} to {my text item delimiters, "."}
  	set strStem to first text item of strOOName
  	set my text item delimiters to dlm
  	tell application id "OOut"
  		activate
  		set strOutPath to (POSIX path of ¬
  			(choose file name with prompt pTitle default name strStem & "." & pstrExtn default location strOutFolder))
  	end tell
  	WriteText2Path(strFT, strOutPath)
  	return strOutPath
  end ChooseFilePathAndSave
  
  -- DOCUMENT REPRESENTED AS A NESTED SET OF {KEY, VALUE} LISTS
  on OODoc2Lst(oDoc, pblnSeln)
  	tell application id "OOut"
  		-- GET THE USER COLUMNS 
  		tell oDoc
  			set lstTypeCols to columns where its name = "ft_type" and its type = popup
  			set idType to missing value
  			if length of lstTypeCols > 0 then set idType to id of item 1 of lstTypeCols
  			
  			set {idTopic, idNote} to {id of topic column, id of note column}
  			set {lstColumns, lstColID, lstColType} to {name, id, type} of (columns where (its id is not idTopic) and (its id is not idNote) and (its id is not idType))
  		end tell
  		
  		-- GET THE ROWS 
  		tell oDoc
  			if pblnSeln then
  				set refSeln to a reference to selected rows
  				if (count of refSeln) < 1 then
  					set lstRoots to (rows of oDoc where level = 1) as list
  				else
  					set lstRoots to my RootsOnly(oDoc, contents of refSeln)
  				end if
  			else
  				set lstRoots to (rows of oDoc where level = 1) as list
  			end if
  			set lstRows to my Rows2List(oDoc, lstRoots, lstColID, lstColumns, lstColType, idType)
  		end tell
  		return lstRows
  	end tell
  end OODoc2Lst
  
  on RootsOnly(oDoc, lstRows)
  	tell application id "OOut"
  		set lstSeen to {}
  		tell oDoc
  			repeat with oRow in lstRows
  				-- only add it if none of its ancestors are in the list
  				set lstAncestors to id of ancestors of oRow
  				set blnSeen to false
  				repeat with oID in lstAncestors
  					set strID to contents of oID
  					if lstSeen contains strID then
  						set blnSeen to true
  						exit repeat
  					end if
  				end repeat
  				if not blnSeen then
  					set strID to id of oRow
  					if not (lstSeen contains strID) then set end of lstSeen to strID
  				end if
  			end repeat
  			repeat with i from 1 to length of lstSeen
  				set item i of lstSeen to row id (item i of lstSeen)
  			end repeat
  			return lstSeen
  		end tell
  	end tell
  end RootsOnly
  
  
  -- TRANSLATE DATA FROM OO3 ROWS INTO NESTED APPLESCRIPT LISTS
  on Rows2List(oDoc, lstRows, lstColID, lstColNames, lstColType, idType)
  	tell application id "OOut"
  		set lstKeyValue to {}
  		
  		set strNumbered to item piOrdered of plstTypes
  		repeat with i from 1 to length of lstRows
  			tell item i of lstRows
  				-- IF THERE IS AN ID_TYPE COLUMN, GET ANY TYPE SPECIFIER
  				set strType to ""
  				if idType is not missing value then
  					set strType to value of cell id idType
  					if strType is missing value then set strType to ""
  				end if
  				
  				-- IF THE NODE IS NUMBERED/ORDERED, GET ITS INDEX
  				if (strType = strNumbered) or (value of (attribute pAttribNumbered of its style) ≠ pstrNoNumber) then
  					set lngIndex to index
  				else
  					set lngIndex to 0
  				end if
  				
  				-- GET THE BUILT-IN PROPERTIES WHICH WE NEED
  				-- Translate **bold** and *italic/oblique* fonts to Markdown emphasis ?
  				if pblnTopicEmphasis then
  					---- Try to enclose any emphasized text in one or two asterisks
  					set {lstText, lstFont} to {text, font} of attribute runs of topic
  					set strTopic to my MDEmphasis(lstText, lstFont)
  				else
  					set strTopic to topic as string
  				end if
  				
  				if pblnNoteEmphasis then
  					---- Try to enclose any emphasized text in one or two asterisks
  					set {lstText, lstFont} to {text, font} of attribute runs of note
  					set strNote to my MDEmphasis(lstText, lstFont)
  				else
  					set strNote to note as string
  				end if
  				
  				set lstRow to {my OneLine(strTopic, pstrLineDelim), paragraphs of strNote, ((state as string) = "checked"), strType, lngIndex}
  				
  				-- GET ANY VALUES IN OTHER USER-DEFINED COLUMNS
  				set lstKeyVal to {}
  				if pblnUserColumns then
  					repeat with iCol from 1 to length of lstColID
  						set varVal to value of cell id (item iCol of lstColID)
  						if (varVal ≠ "" and varVal is not missing value) then
  							set varVal to my FormatVal(varVal, (item iCol of lstColType) as string)
  							if varVal ≠ false then set end of lstKeyVal to {item iCol of lstColNames, varVal}
  						end if
  					end repeat
  				end if
  				set end of lstRow to lstKeyVal
  				
  				-- AND ANY CHILD ROWS
  				set lstChiln to (its children) as list
  				if (length of lstChiln) > 0 then
  					set end of lstRow to my Rows2List(oDoc, lstChiln, lstColID, lstColNames, lstColType, idType)
  				else
  					set end of lstRow to {}
  				end if
  			end tell
  			set item i of lstRows to lstRow
  		end repeat
  		return lstRows
  	end tell
  end Rows2List
  
  on MDEmphasis(lstText, lstFont)
  	set str to ""
  	repeat with i from 1 to length of lstText
  		set strText to item i of lstText
  		if strText ≠ "" then
  			set strFont to item i of lstFont
  			if strFont contains "BoldOblique" or strFont contains "BoldItalic" then
  				set str to str & my emphasize(strText, "***")
  			else if strFont contains "bold" then
  				set str to str & my emphasize(strText, "**")
  			else if strFont contains "oblique" or strFont contains "italic" then
  				set str to str & my emphasize(strText, "*")
  			else
  				set str to str & strText
  			end if
  		end if
  	end repeat
  	if str contains "* *" then set str to PurgeMDGaps(str)
  	return str
  end MDEmphasis
  
  -- "**and** **yet** **the**" → **and yet the**
  on PurgeMDGaps(str)
  	DblReplace(str, "\\([0-9A-Za-z]\\)\\* \\*\\([0-9A-Za-z]\\)", "\\([0-9A-Za-z]\\)\\*\\* \\*\\*\\([0-9A-Za-z]\\)", "\\1 \\2")
  end PurgeMDGaps
  
  on DblReplace(str, strFind, strFind2, strReplace)
  	do shell script "echo " & quoted form of str & " | sed -e 's/" & strFind & "/" & strReplace & "/g' | sed -e 's/" & strFind2 & "/" & strReplace & "/g'"
  end DblReplace
  
  
  -- wrap the trimmed part of the string with MD emphasis markers
  -- and restore the end spaces
  on emphasize(strText, strMarker)
  	set {dlm, my text item delimiters} to {my text item delimiters, space}
  	set lstParts to text items of strText
  	set lngParts to length of lstParts
  	-- The opening marker
  	repeat with i from 1 to lngParts
  		set strPart to item i of lstParts
  		if strPart ≠ "" then
  			set item i of lstParts to strMarker & strPart
  			exit repeat
  		end if
  	end repeat
  	-- and the closing marker
  	repeat with i from lngParts to 1 by -1
  		set strPart to item i of lstParts
  		if strPart ≠ "" then
  			set item i of lstParts to strPart & strMarker
  			exit repeat
  		end if
  	end repeat
  	set str to lstParts as string
  	set my text item delimiters to dlm
  	return str
  end emphasize
  
  -- Format dates (2012-11-30 22:13) and durations (4h 30m 20s), etc for FoldingText tag values
  -- Note that for OmniOutliner a "day" may be only 8h -- See Inspectors > Column Type > Duration Settings
  on FormatVal(varVal, strType)
  	if strType = "rich text" then
  		return OneLine(varVal, pstrLineDelim)
  	else if strType = "date" then
  		-- 2012-10-31 08:36
  		tell varVal
  			set {dlm, my text item delimiters} to {my text item delimiters, "-"}
  			tell varVal
  				set strDate to {its year as string, my PadNum((its month as integer), 2), my PadNum(its day, 2)} as string
  				set my text item delimiters to ":"
  				set strDate to strDate & space & ({my PadNum(its hours as string, 2), my PadNum(its minutes, 2)} as string)
  			end tell
  			set my text item delimiters to dlm
  			return strDate
  		end tell
  	else if strType = "duration" then
  		-- (4h 30m 20s)
  		set lngSecs to varVal * hours
  		set lst to {}
  		if lngSecs > days then
  			set lngUnits to (lngSecs div days) as integer
  			set end of lst to ((lngUnits as string) & "d")
  			set lngSecs to lngSecs mod days
  		end if
  		if lngSecs > hours then
  			set lngUnits to (lngSecs div hours) as integer
  			set end of lst to ((lngUnits as string) & "h")
  			set lngSecs to lngSecs mod hours
  		end if
  		if lngSecs > 60 then
  			set lngUnits to (lngSecs div 60) as integer
  			set end of lst to ((lngUnits as string) & "m")
  			set lngSecs to (lngSecs mod 60) as integer
  		end if
  		if lngSecs > 0 then set end of lst to ((lngSecs as string) & "s")
  		
  		set {dlm, my text item delimiters} to {my text item delimiters, space}
  		set strDurn to lst as string
  		set my text item delimiters to dlm
  		return strDurn
  	else if strType = "checkbox" then
  		-- true/false
  		return (varVal = "checked")
  	else
  		return varVal as string
  	end if
  end FormatVal
  
  -- COERCE AN .OO3 NODE TEXT TO A SINGLE PARA 
  on OneLine(strNode, strDelimiter)
  	set lstLines to paragraphs of strNode
  	if length of lstLines > 1 then
  		set {dlm, my text item delimiters} to {my text item delimiters, strDelimiter}
  		set strNode to lstLines as string
  		set my text item delimiters to dlm
  	end if
  	return strNode
  end OneLine
  
  -- WRITE UTF-8 TEXT FILE TO POSIX PATH
  on WriteText2Path(strText, strPosixPath)
  	set f to (POSIX file strPosixPath)
  	open for access f with write permission
  	write strText as «class utf8» to f
  	close access f
  end WriteText2Path
  
  -- RETRIEVE PREFIX FOR A PARTICULAR NODE TYPE
  on GetPrefix(strType, lngIndex)
  	if lngIndex > 0 then
  		return ((lngIndex as integer) as string) & ". "
  	else
  		repeat with i from 1 to length of plstTypes
  			if item i of plstTypes = strType then return item i of plstPrefixes
  		end repeat
  		return ""
  	end if
  end GetPrefix
  
  -- N TABS OR HASHES
  on stringof(lngChars, strChar)
  	set str to ""
  	repeat with i from 1 to lngChars
  		set str to str & strChar
  	end repeat
  	return str
  end stringof
  
  -- PREPEND ZEROS TO BRING A DIGIT STRING TO A GIVEN LENGTH
  on PadNum(lngNum, lngDigits)
  	set strNum to lngNum as string
  	set lngGap to (lngDigits - (length of strNum))
  	repeat while lngGap > 0
  		set strNum to "0" & strNum
  		set lngGap to lngGap - 1
  	end repeat
  	strNum
  end PadNum
  
  on Notify(strAppName, strProcess, strTitle, strMsg)
  	tell application "System Events"
  		set strGrowlApp to ""
  		repeat with oGrowlApp in {"Growl", "GrowlHelperApp"}
  			if (count of (every process whose name = oGrowlApp)) > 0 then
  				set strGrowlApp to oGrowlApp
  				exit repeat
  			end if
  		end repeat
  		if strGrowlApp ≠ "" then
  			set strScript to "
  			tell application \"" & strGrowlApp & "\"
  				register as application \"Houthakker scripts\" all notifications {\"" & strProcess & "\"} default notifications {\"" & strProcess & "\"} icon of application \"" & strAppName & "\"
  				notify with name \"" & strProcess & "\" title \"" & strTitle & "\" application name \"Houthakker scripts\" description \"" & strMsg & "\"
  			end tell"
  			strScript
  			run script strScript
  		else
  			activate
  			display dialog strMsg buttons {"OK"} default button "OK" with title pTitle & tab & pVer
  		end if
  	end tell
  end Notify
  