Interest in Community-Developed Documentation Comment Standard for AppleScript

A few years ago, I submitted a feature request for Script Debugger 9 that would allow for easy lookup of library handler information (e.g. description, parameters, return type) at the call site. This would serve to make up for some of AppleScript’s limitations around default parameters & enumerations that limit the flexibility of script library handlers.

Unfortunately, with the sad news that Script Debugger has reached its end-of-life, this feature request will not see the light of day.

I want to gauge whether there’s any community interest in making a documentation comment standard for AppleScript handlers, as well as a robust script that can pull up these documentation comments when coding in Script Debugger.


I’ve made a proof-of-concept script to illustrate how this would be used. In it’s current form, it’s neither robust nor performance-optimized, but it does show that this approach will work.

To use the lookup script, you just place the cursor in (or select) the name of the library handler & run the lookup script. A window pops up with the documentation comment for the handler.

The script looks for the documentation comment in the library by checking if there is a multi-line comment starting on the line immediately after the handler definition line.

If you want to test this for yourself, you can download the Docstring Test Library (which contains the example handler replaceText, the lookup script functionality, and some support handlers).

  • Install the script library at ~/Library/Script Libraries/Docstring Test Library.scpt (note the extension).
  • Call the lookup script with e.g. FastScripts, or by placing an alias to the library in Script Debugger’s script menu folder & clicking it in the menu.
Example Calling Script
use DTL : script "Docstring Test Library"

set s to "Now is the winter of our discontent."
set stronger_s to DTL's replaceText(s, ".", "!")
Docstring Test Library
### EXAMPLE HANDLER (being used by the script consuming the library - the lookup handler will pull this docstring)
to replaceText(s, search_string, replacement_string)
	(*    (string OR list of strings, string OR list of strings, string OR list of strings) → string OR list of strings
	
	Return a copy of s with all instances of search_string replaced with replacement_string. s may be either a string or a list of strings. Search_string may be a list containing multiple substrings to be replaced by replacement_string; if replacement_string is also a list of substrings, then each substring in search_string will be sequentially replaced with the corresponding substring in replacement_string.
	
	Parameters:
	s [string OR list of strings] : The string(s).
	search_string [string OR list of strings] : The substring, or list of substrings, to be replaced.
	replacement_string [string OR list of strings] : The substring, or list of substrings, to replace the search string(s).
	
	Result:
	[string OR list of strings] : A copy of s with the appropriate substitutions made.    *)
	
	if class of replacement_string is string then
		set search_string to {search_string}
		set replacement_string to {replacement_string}
	else if class of search_string is string then
		set search_string to {search_string}
	end if
	
	if class of s is string then
		set previous_TIDs to AppleScript's text item delimiters
		repeat with i from 1 to length of replacement_string
			set AppleScript's text item delimiters to item i of search_string
			set s_text_items to text items of s
			set AppleScript's text item delimiters to item i of replacement_string
			set s to s_text_items as string
		end repeat
		set AppleScript's text item delimiters to previous_TIDs
		
		return s
	else -- s is a list of strings.
		set s_length to length of s
		
		set new_string_list to {}
		repeat with i from 1 to s_length
			set the end of new_string_list to item i of s
		end repeat
		
		set previous_TIDs to AppleScript's text item delimiters
		repeat with i from 1 to length of replacement_string
			set AppleScript's text item delimiters to item i of search_string
			repeat with j from 1 to s_length
				set item j of new_string_list to text items of item j of new_string_list
			end repeat
			set AppleScript's text item delimiters to item i of replacement_string
			repeat with j from 1 to s_length
				set item j of new_string_list to item j of new_string_list as string
			end repeat
		end repeat
		set AppleScript's text item delimiters to previous_TIDs
		
		return new_string_list
	end if
	
end replaceText



### LOOKUP HANDLERS (Also handlers running the lookup functions in this condensed example).
return lookUpSelectedHandler()

to getHandlerInfoFromScriptDebugger()
	tell application "Script Debugger"
		set current_document_id to id of document 1
		set current_document to a reference to document id current_document_id
		set {{selection_offset, selection_length}, selection_contents} to {character range, contents} of selection of current_document
		set script_text to source text of current_document
		set handler_end to (my getOffsetWithEndpoints("(", script_text, selection_offset, -1)) - 1
		set handler_start to (my getLastOffsetWithEndpoints(space, script_text, 1, selection_offset)) + 1
		set library_end to handler_start - 2
		set library_start to (my getLastOffsetWithEndpoints(space, script_text, 1, library_end)) + 1
		set handler_name to text handler_start thru handler_end of script_text
		set library_token to text library_start thru library_end of script_text
		set library_token to my stripString(library_token, "(-")
		if library_token is not in {"my", "its"} then
			set library_token to text 1 thru -3 of library_token
			set library_specifier to (script property library_token of current_document)'s source text
			set library_name to text 9 thru -2 of library_specifier
		end if
	end tell
	return {library_name, handler_name}
end getHandlerInfoFromScriptDebugger


to lookUpSelectedHandler()
	set {library_name, handler_name} to getHandlerInfoFromScriptDebugger()
	
	set handler_search to "\nto " & handler_name & "("
	set decompiled_library to linefeed & my decompileScriptFile("/Users/David/Library/Mobile Documents/com~apple~CloudDocs/Library/Script Libraries/" & library_name & ".scpt")
	set handler_offset to my getOffset(handler_search, decompiled_library)
	if handler_offset = 0 then return "HANDLER NOT FOUND"
	set handler_offset to handler_offset + 1
	set handler_start to handler_offset + 3
	
	set linefeed_found to false
	set docstring_start to 0
	repeat with i from handler_offset to length of decompiled_library
		set char to item i of decompiled_library
		set char_is_linefeed to char is linefeed
		if char_is_linefeed then
			set linefeed_found to true
			set handler_end to i - 1
		else
			if linefeed_found then
				if text i thru (i + 2) of decompiled_library is "\t(*" then
					set docstring_start to i
				else
					exit repeat -- No docstring.
				end if
			else
				-- Continue searching.
			end if
		end if
	end repeat
	set handler_signature to text handler_start thru handler_end of decompiled_library
	
	if docstring_start is 0 then return "DOCSTRING NOT FOUND"
	set docstring_end to (my getOffsetWithEndpoints("*)", decompiled_library, docstring_start, -1)) + 1
	set docstring to text (docstring_start + 7) thru (docstring_end - 6) of decompiled_library
	
	set dialog_displayer to current application -- Don't block the UI if run by Script Debugger.
	if id of dialog_displayer starts with "com.latenightsw.ScriptDebugger" then
		set dialog_displayer to application "System Events" -- FastScripts is a better alternative, if installed.
	end if
	
	tell dialog_displayer to display dialog tab & handler_signature & "\n\n\t" & docstring with title (handler_name & " — " & library_name) buttons {"Dismiss"} default button 1
end lookUpSelectedHandler



### SUPPORT HANDLERS (Not relevant to example, but used in the current implementation of the lookup script)
to sliceString(s, slice_start, slice_end)
	(*    (string, integer, integer) → string
	
	Return a string containing characters slice_start to slice_end of s. Unlike native AppleScript text slicing, slice indices can be out of range, and slicing from a greater index to a lesser index returns an empty string.
	
	Parameters:
	s [string] : The string.
	slice_start [integer] : The starting slice index.
	slice_end [integer] : The ending slice index.
	
	Result:
	[string] : A slice of s from slice_start to slice_end.    *)
	
	set s_length to length of s
	if s_length is 0 then return s -- String is empty.
	
	if slice_start is greater than 0 then
		if slice_start is greater than s_length then return "" -- Start is too large.
		if slice_end is less than 0 then
			-- Positive slice_start & negative slice_end.
			if slice_end is less than -s_length then return "" -- End is too small.
			if slice_start is greater than slice_end + s_length + 1 then return "" -- End is before start.
		else
			-- Positive slice_start & positive slice_end.
			if slice_start is greater than slice_end then return "" -- End is before start.
			if slice_end is greater than s_length then set slice_end to s_length -- End is too large.
		end if
	else
		if slice_start is 0 then set slice_start to -s_length -- Start is 0.
		if slice_end is less than 0 then
			-- Negative slice_start & negative slice_end.
			if slice_end is less than -s_length then return "" -- End is too small.
			if slice_end is less than slice_start then return "" -- End is before start.
		else
			-- Negative slice_start & positive slice_end.
			if slice_start is greater than slice_end - s_length - 1 then return "" -- End is before start.
			if slice_end is 0 then set slice_end to s_length -- End is 0.
			if slice_end is greater than s_length then set slice_end to s_length -- End is too large.
		end if
		if slice_start is less than -s_length then set slice_start to 1 -- Start is too small.
	end if
	
	return text slice_start thru slice_end of s
	
end sliceString


to getOffset(search_string, s)
	(*    (string, string) → integer
	
	Return the index of the first occurrence of search_string in s, or 0 if search_string cannot be found.
	
	Parameters:
	search_string [string] : The substring to search for.
	s [string] : The string in which to search.
	
	Result:
	[integer] : The first index of search_string in s, or 0.    *)
	
	set s_length to length of s
	if s_length is 0 or length of search_string is 0 then return 0
	
	set previous_TIDs to AppleScript's text item delimiters
	set AppleScript's text item delimiters to search_string
	set search_string_offset to length of first text item of s
	set AppleScript's text item delimiters to previous_TIDs
	
	if search_string_offset is s_length then
		return 0
	else
		return search_string_offset + 1
	end if
	
end getOffset


to getOffsetWithEndpoints(search_string, s, search_start, search_end)
	(*    (string, string, integer, integer) → integer
	
	Return the index of the first occurrence of search_string in the slice of s from character search_start to character search_end, or 0 if search_string is not in the slice. Character indices are interpreted as in the sliceString function.
	
	Parameters:
	search_string [string] : The substring to search for.
	s [string] : The string in which to search.
	search_start [integer] : The index at which to start the search.
	search_end [integer] : The index at which to end the search.
	
	Result:
	[integer] : The offset of the first occurrence of search_string in s sliced from search_start to search_end, or 0 if search_string cannot be found.    *)
	
	set sliced_s to sliceString(s, search_start, search_end)
	set sliced_s_length to length of sliced_s
	
	if sliced_s_length is 0 or length of search_string is 0 then return 0
	
	set previous_TIDs to AppleScript's text item delimiters
	set AppleScript's text item delimiters to search_string
	set search_string_offset to length of first text item of sliced_s
	set AppleScript's text item delimiters to previous_TIDs
	
	if search_string_offset is sliced_s_length then
		return 0
	else
		if search_start is less than 1 then
			set s_length to length of s
			if search_start is 0 or search_start is less than -s_length then
				return search_string_offset + 1
			else
				return search_string_offset + search_start + s_length + 1
			end if
		else
			return search_string_offset + search_start
		end if
	end if
	
end getOffsetWithEndpoints


to getLastOffsetWithEndpoints(search_string, s, search_start, search_end)
	(*    (string, string, integer, integer) → integer
	
	Return the index of the last occurrence of search_string in the slice of s from character search_start to character search_end, or 0 if search_string is not in the slice. Character indices are interpreted as in the sliceString function.
	
	Parameters:
	search_string [string] : The substring to search for.
	s [string] : The string in which to search.
	search_start [integer] : The index at which to start the search.
	search_end [integer] : The index at which to end the search.
	
	Result:
	[integer] : The offset of the last occurrence of search_string in s sliced from search_start to search_end, or 0 if search_string cannot be found.    *)
	
	set sliced_s to sliceString(s, search_start, search_end)
	
	set sliced_s_length to length of sliced_s
	set search_string_length to length of search_string
	
	if sliced_s_length is 0 or search_string_length is 0 then return 0
	
	set previous_TIDs to AppleScript's text item delimiters
	set AppleScript's text item delimiters to search_string
	set search_string_offset to length of last text item of sliced_s
	set AppleScript's text item delimiters to previous_TIDs
	
	if search_string_offset is sliced_s_length then
		return 0
	else
		if search_start is less than 1 then
			set s_length to length of s
			if search_start is 0 or search_start is less than -s_length then
				return sliced_s_length - search_string_offset - search_string_length + 1
			else
				return sliced_s_length - search_string_offset - search_string_length + 1 + search_start + s_length
			end if
		else
			return sliced_s_length - search_string_offset - search_string_length + search_start
		end if
	end if
	
end getLastOffsetWithEndpoints


to stripString(s, strip_characters)
	(*    (string, string OR {string, string}) → string
	
	Return s with all leading and/or trailing strip_characters removed. If strip_characters is a string, then remove the specified characters from both ends of s. If strip_characters is a two-string list, then strip the characters in the first string from the start of s and the characters in the second string from the end of s. If strip_characters is empty, then strip all whitespace from both ends of s.
	
	Parameters:
	s [string] : The string to strip.
	strip_characters [string OR {string, string}] : The characters to remove from the ends of s. If a string or single-item list, remove those characters from both ends of s. If a multiple-string list, remove the characters in the first string from the start of s and the characters in the last string from the end of s. If empty, remove all whitespace characters from both ends.
	
	Result:
	[string] : A copy of s with strip_characters removed from one or both ends.    *)
	
	set s_length to length of s
	
	if s_length is 0 then return ""
	if length of strip_characters is 0 then set strip_characters to WHITESPACE
	
	if class of strip_characters is list then
		set left_strip_characters to item 1 of strip_characters
		set right_strip_characters to item -1 of strip_characters
	else
		set left_strip_characters to strip_characters
		set right_strip_characters to strip_characters
	end if
	
	repeat with i from 1 to s_length
		if character i of s is not in left_strip_characters then exit repeat
	end repeat
	
	repeat with j from 1 to (s_length + 1 - i)
		if character -j of s is not in right_strip_characters then exit repeat
	end repeat
	
	set s to text i thru -j of s
	if s is in left_strip_characters or s is in right_strip_characters then set s to ""
	
	return s
	
end stripString


to decompileScriptFile(file_path)
	(*    (string) → string
	
	Decompile the script file at file_path and return it as plain text.
	
	Parameters:
	file_path [string] : The POSIX path of the script file to decompile.
	
	Result:
	[string] : The plain-text representation of the script file at file_path.    *)
	
	set script_result to do shell script "osadecompile " & quoted form of file_path without altering line endings
	if script_result ends with linefeed then
		if script_result is linefeed then
			return ""
		else
			return text 1 thru -2 of script_result
		end if
	else
		return script_result
	end if
	
end decompileScriptFile

Right now, the proof-of-concept lacks error checking & requires the exact syntax used in the example, but the parsing can easily be expanded to cover most valid AppleScript syntax.


My questions for the community are as follows:

  1. Is there sufficient interest in this feature to standardize documentation comments & develop a robust lookup script?
  2. Does anyone have any fundamental design changes they think would be useful (e.g. using some kind of persistent floating window instead of using display dialog)?

I understand your suggestion as bellow.

I made some documentation script for AppleScript users. It is a kind of online help.
It checks [CTRL] key in its executing process.
If [CTRL] pressed, the script read its description contents and display it.
Description contains outilne of script.
It use my display text field script library because it can display large and colored text.
My target environment is Script Menu, FastScript3 or the other script runners.