In which we finally get to create our blog post and open it in BBEdit.

Following on from our previous chunk (Applescript Jekyll assistant (pt 4) )

Questions arising from this exercise:

  1. Was AppleScript the most appropriate tool to achieve our Applescript Jekyll assistant?

  2. Since there was relatively little sending a receiving of Apple Events, and quite a lot of string manipulation, would be have been better off using the Javascript alternative in Script Editor?

  3. Would we have been better off doing most of our stuff in a Python program and calling it from a minimal AppleScript that mostly dealt with the inter-application Apple Events?

Image of end result Image of end result with body text selected, ready to edit

Open in Script Editor

-- these two properties are immutable 
property homeFolder : the POSIX path of (path to home folder)
property plistPath : the POSIX path of (homeFolder & ".makepost.plist")
(*
	We keep three persistent settings in 'makepost.plist':
	
	'chosenBlogPath' is the path to the folder where Jekyll expects blog posts to be.
	This is typically the '_posts' subdirectory in the Jekyll project files.
	
	'availableCategories' is a list of the user's Categories from which the user can choose
	and which will appear in the frontmatter of the newly-to-be-created blog post's 'markdown'
	
	'chosenScriptFilePath' is the path to where the user has stored 'makepost.py'
	which is written to be used as a shell command for creating new Jekyll blog posts
	
*)
-- These 5 properties of our Script Object are mutable

-- persistent across runs
property chosenBlogPath : the POSIX path of homeFolder
property availableCategories : {"Blog", "Scripting"}
property chosenScriptFilePath : the POSIX path of (homeFolder & "bin/makepost.py")

-- not persistent across runs
property chosenTitle : "Untitled"
property chosenCategories : availableCategories

(*	
	Initialisation
	-- -- -- -- --

	The plist, and thus the three persistent settings above may not yet exist
	in which case we have bootstrapped them with some sensible initial values.
	
	Notes:
	* AppleScript aliases have to refer to existing files
	
	* POSIX files and paths need not refer to existing files or directories
	
	* The AppleScript path of a POSIX path is "text"
*)
-- overwite our bootstrapped properties if the user already has a .plist
initialisePersistentSettings()


(*
	Next, we ask the user
	1. What the title of her blog is going to be;
		and from this, we will derive the Jekyll-conformant file name for the post
	2. Which Folder she uses for posts
		in her Jekyll-conformant Folder structure
*)
-- overwite our chosenTitle and chosenBlogPath properties as the user chooses
chooseTitleAndBlogpath()


(*
	Ask the user to choose which blog categories the post is to be part of.
	getCats() will deal with non-existent .blogcats
*)
set chosenCategories to chooseCategories()

(*
	An insanity check!
	Always worthwhile!
	... for prolonged sanity
*)
sanityCheck()

(*
	We must be about ready to invoke Makepost.py
*)
-- on shell script error: AppleScript will exit after having reported makepost's STDERR text
set resultFromMakepost to do shell script buildshellCommand()

--	edit the newly-created post in BBEdit if she has it installed
if applicationIsInstalled("BBEdit") then
	editInBBEdit(resultFromMakepost)
else
	display alert Â
		"Your blog post file has been written to " message resultFromMakepost
end if


-- DEBUG -- DEBUG -- DEBUG -- DEBUG -- DEBUG -- DEBUG -- DEBUG -- DEBUG --
log "chosenTitle: " & chosenTitle
log "chosenBlogPath: " & chosenBlogPath
log "chosenScriptFilePath: " & chosenScriptFilePath
repeat with n from 1 to the length of availableCategories
	log "availableCategories (" & n & "): " & item n of availableCategories
end repeat
repeat with n from 1 to the length of chosenCategories
	log "chosenCategories (" & n & "): " & item n of chosenCategories
end repeat
log "buildshellCommand: " & buildshellCommand()
log "resultFromMakepost: " & resultFromMakepost
-- DEBUG -- DEBUG -- DEBUG -- DEBUG -- DEBUG -- DEBUG -- DEBUG -- DEBUG --


-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
(*
	Handlers / subroutines
*)
-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 

to editInBBEdit(fileToBeEdited)
	--	edit the newly-created post in BBEdit if she has it installed
	
	tell application "BBEdit"
		activate
		open POSIX file chosenBlogPath -- the "project window"
		open POSIX file fileToBeEdited opening in project window 1
		tell text of front text window
			find "^Replace.*$" options {starting at top:true, search mode:grep, wrap around:false, backwards:false} with selecting match
		end tell
		
	end tell
	
end editInBBEdit

on applicationIsInstalled(appRef) -- (String) as Boolean
	-- Thanks to Stackoverflow user scolfax
	-- https://stackoverflow.com/questions/14297644/check-if-an-application-exists-using-applescript
	set shellScript to "osascript -e " & ("id of application \"" & appRef & "\"")'s quoted form & " || osascript -e " & ("id of application id \"" & appRef & "\"")'s quoted form & " || :"
	-- 	e.g.: osascript -e 'id of application "BBEdit"' || osascript -e 'id of application id "BBEdit"' || :	
	-- 	NB The command to the right of the || (logical OR) is executed only when the first command fails
	--	If one of the commands succeeds, the result will be a string identifying
	--	either the application "BBEdit" or the application id "BBEdit"
	set appBundleId to do shell script shellScript
	return (appBundleId ­ "")
end applicationIsInstalled

to buildshellCommand()
	--  the `--cryptic` option was designed especially for us: 
	-- `makepost.py` is quiet except for printing the file name of the newly-created post
	return "python3 " & chosenScriptFilePath & space & quoted form of chosenTitle & space & makeCatArgs(chosenCategories) & space & "  --cryptic "
end buildshellCommand

to sanityCheck()
	
	set blogFolderPath to chosenBlogPath
	if not FolderExists(chosenBlogPath) then
		
		-- this shoudn't happen as, in the `choose folder` dialogue, the user pointed to an existing folder
		if button returned of (display dialog "The folder of blog posts " & blogFolderPath & " does not (or no longer) exists" buttons {"Choose again", "Cancel"} default button 1 cancel button 2 with title "Missing folder for blog posts" with icon caution) is "Choose again" then
			set chosenBlogPath to homeFolder
			chooseBlogPath()
		else
			error "Error: The folder of blog posts " & blogFolderPath & " does not (or no longer) exists"
		end if
	end if
	
	-- Ensure we know in which directory the Python script `makepost.py` has been installed
	
	if not FileExists(chosenScriptFilePath) then
		
		-- get the user to tell us where to find the executable
		-- either she hasn't told us yet, or she may have moved it since last use
		set selectedScriptFilePath to Â
			(choose file with prompt "Please locate makepost.py in the directory where it has been installed" of type {"public.python-script"})
		set chosenScriptFilePath to the POSIX path of selectedScriptFilePath
		persistChosenScriptFilePath(chosenScriptFilePath)
	end if
	
end sanityCheck

to chooseCategories()
	
	repeat
		
		choose from list availableCategories with title "Choose Blog Categories" with prompt "Choose one or more Blog Categories" OK button name "Save" cancel button name "Edit" default items {"Blog"} with multiple selections allowed
		
		if the result is not false then
			return result
		else
			set availableCategories to editCats()
		end if
		
	end repeat
	
end chooseCategories

to editCats()
	-- Ask the user to edit her blog categories
	
	-- turn the list availableCategories into a comma-separated string for her to edit
	set availableCatString to convertListToString(availableCategories, ",")
	set {catButton, editedCategories} to {button returned, text returned} of (display dialog "Add and delete comma-separated values. Best not to have too many!" default answer availableCatString buttons {"Save", "Cancel"} default button 1 cancel button 2 with title "Edit your Categories")
	-- AppleScript will exit if user presses the "Cancel" button
	
	--  just in case we have a zero length "list"
	if editedCategories is "" then set editedCategories to "Blog"
	
	-- sanitise the new category "list" which is a string
	-- make it a list
	set editedCategoriesList to convertStringToList(editedCategories, ",")
	
	-- remove any leading or trailing spaces of each list item
	set trimmedEditedCategoriesList to trimItems(editedCategoriesList)
	
	-- so we can set a default in the choose cats dialogue
	if trimmedEditedCategoriesList does not contain "Blog" then
		copy "Blog" to end of trimmedEditedCategoriesList
	end if
	
	set sortedTrimmedEditedCategoriesList to sortList(trimmedEditedCategoriesList)
	
	-- write the newly-edited, and sorted, list of categories to the plist
	persistCats(sortedTrimmedEditedCategoriesList)
	
	return sortedTrimmedEditedCategoriesList
	
end editCats

to convertListToString(theList, theDelimiter)
	set AppleScript's text item delimiters to theDelimiter
	set theString to theList as string
	set AppleScript's text item delimiters to ""
	return theString
end convertListToString

to convertStringToList(theText, theDelimiter) -- (String) as list
	set AppleScript's text item delimiters to theDelimiter
	set theListItems to every text item of theText
	set AppleScript's text item delimiters to ""
	return theListItems
end convertStringToList

to trimItems(inList)
	
	set outList to {}
	repeat with i from 1 to (count (items of inList))
		set trimmedText to trimText((item i of inList), " ", "both")
		if trimmedText is not "" then
			if countInstancesOfItemInList(outList, trimmedText) is 0 then
				copy trimmedText to end of outList
			end if
		end if
	end repeat
	
	return outList
	
end trimItems

to trimText(theText, theCharactersToTrim, theTrimDirection)
	set theTrimLength to length of theCharactersToTrim
	if theTrimDirection is in {"beginning", "both"} then
		repeat while theText begins with theCharactersToTrim
			try
				set theText to characters (theTrimLength + 1) thru -1 of theText as string
			on error
				-- text contains nothing but trim characters
				return ""
			end try
		end repeat
	end if
	if theTrimDirection is in {"end", "both"} then
		repeat while theText ends with theCharactersToTrim
			try
				set theText to characters 1 thru -(theTrimLength + 1) of theText as string
			on error
				-- text contains nothing but trim characters
				return ""
			end try
		end repeat
	end if
	return theText
end trimText

to getCats()
	
	tell application "System Events"
		
		tell property list file plistPath
			set availableCategories to value of property list item "catsList"
		end tell
		
	end tell
	
end getCats

to persistChosenScriptFilePath(theFilePath)
	
	tell application "System Events"
		
		tell property list file plistPath
			set value of property list item "scriptFilePath" to theFilePath
		end tell
		
	end tell
	
end persistChosenScriptFilePath

to persistCats(theCategoriesList)
	
	tell application "System Events"
		
		tell property list file plistPath
			set value of property list item "catsList" to theCategoriesList
		end tell
		
	end tell
	
end persistCats

to chooseTitleAndBlogpath()
	
	-- Reduce the length of the path for displaying as the legend in button 1
	set pathClue to crypticPath(chosenBlogPath)
	set chosenTitle to "Untitled"
	
	-- We'll cycle the path-choosing, not forgetting the title-choosing each cycle, until user is happy with her choice
	repeat
		set {OKButton, chosenTitle} to {button returned, text returned} of (display dialog "Choose the Title of your new post" default answer chosenTitle with title "Makepost" buttons {pathClue, "Other"} default button 1)
		
		if OKButton is pathClue then
			-- stay with the blogpath that we read from user's settings file
			exit repeat
		else
			chooseBlogPath()
			copy crypticPath(chosenBlogPath) to pathClue
		end if
		
	end repeat
	
end chooseTitleAndBlogpath

to chooseBlogPath()
	
	-- User chooses her  blog posts Folder
	set chosenBlogPath to the POSIX path of (choose folder with prompt "Choose the path to your blog posts Folder" default location chosenBlogPath)
	
	-- record chosenBlogPath in makepost's persistent settings
	tell application "System Events"
		tell property list file plistPath
			set value of property list item "blogPath" to chosenBlogPath
		end tell
	end tell
end chooseBlogPath

on crypticPath(fullPath) -- (String) as String
	set splitPath to splitText(fullPath, "/")
	set lastBit to {}
	copy item ((count of splitPath) - 2) of splitPath to end of lastBit
	copy "/" to end of lastBit
	copy item ((count of splitPath) - 1) of splitPath to end of lastBit
	
	return ("Place in .../" & lastBit as text) & " ?"
end crypticPath

to splitText(theText, theDelimiter)
	set AppleScript's text item delimiters to theDelimiter
	set theTextItems to every text item of theText
	set AppleScript's text item delimiters to ""
	return theTextItems
end splitText


to initialisePersistentSettings()
	
	if not FileExists(plistPath) then
		
		-- create the plist with bootstrap settings now, and for recording the user's choices later
		tell application "System Events"
			
			-- Have a look at the 'Mac Automation Scripting Guide' for more on this 'tell' block
			
			-- Create an empty property list dictionary item
			set theParentDictionary to make new property list item with properties {kind:record}
			
			-- Create a new property list file using the empty dictionary list item as contents
			set thePropertyListFilePath to plistPath
			
			set thePropertyListFile to make new property list file with properties {contents:theParentDictionary, name:thePropertyListFilePath}
			
			tell property list items of thePropertyListFile
				
				-- use bootstrapped settings of these properties
				
				-- Add a list key for accessing the users' blog categories
				make new property list item at end with properties {kind:list, name:"catsList", value:availableCategories}
				
				-- Add a string key for accessing the path to the user's blog posts
				make new property list item at end with properties {kind:string, name:"blogPath", value:chosenBlogPath}
				
				-- Add a string key for accessing the path to the python script 'makepost.py'
				make new property list item at end with properties {kind:string, name:"scriptFilePath", value:chosenScriptFilePath}
				
			end tell
		end tell
		
	else
		
		-- overwrite the bootstrapped values of these persistent properties
		tell application "System Events"
			
			tell property list file plistPath
				set chosenBlogPath to value of property list item "blogPath"
				set availableCategories to value of property list item "catsList"
				set chosenScriptFilePath to value of property list item "scriptFilePath"
			end tell
			
		end tell
		
	end if
	
end initialisePersistentSettings

on FileExists(theFile) -- (String) as Boolean
	-- Convert the file to a string
	set theFile to theFile as string
	tell application "System Events"
		if exists file theFile then
			return true
		else
			return false
		end if
	end tell
end FileExists

on FolderExists(theFolder) -- (String) as Boolean
	set theFolder to theFolder as string
	tell application "System Events"
		if exists folder theFolder then
			return true
		else
			return false
		end if
	end tell
end FolderExists

on sortList(theList)
	set theIndexList to {}
	set theSortedList to {}
	repeat (length of theList) times
		set theLowItem to ""
		repeat with a from 1 to (length of theList)
			if a is not in theIndexList then
				set theCurrentItem to item a of theList as text
				if theLowItem is "" then
					set theLowItem to theCurrentItem
					set theLowItemIndex to a
				else if theCurrentItem comes before theLowItem then
					set theLowItem to theCurrentItem
					set theLowItemIndex to a
				end if
			end if
		end repeat
		set end of theSortedList to theLowItem
		set end of theIndexList to theLowItemIndex
	end repeat
	return theSortedList
end sortList

on countInstancesOfItemInList(theList, theItem)
	set theCount to 0
	repeat with a from 1 to count of theList
		if item a of theList is theItem then
			set theCount to theCount + 1
		end if
	end repeat
	return theCount
end countInstancesOfItemInList


on makeCatArgs(catsList) -- ({String} -> {String}
	
	set resultCats to {}
	set catPrefix to "--category='"
	
	repeat with i from 1 to (count (items of catsList))
		set properCat to (catPrefix & (item i of catsList) & "' ")
		copy properCat to end of resultCats
	end repeat
	
	return resultCats
	
end makeCatArgs