minor format change

minor format change

-- main_helper
-- Erik Shagdar, NYHTC
-- Helper script that compiles all .applescript files into a folder of compiled .scpt files, then builds the htcLib app to reference those compiled scripts.

(* HISTORY:
	2019-12-11 ( dshockley, eshagdar ): Removed shouldBuildAPP - if some handler changes, we MUST build the app, so no point in asking. Removed unneeded properties from the app build, as well as other unneeded code/comments.
	2019-12-10 ( dshockley ): Changed to COMPILE each handler, then also compile htcLib into the same folder. Only compile handler if the version changes. Also, only compile app if some change was made to handler calls.
	2017-12-18 ( eshagdar ): skip files whose name begins wtih 'WIP_' ( work in progress ).
	2017-11-14 ( dshockley ): open system preference pane even from command line.
	2017-10-20 ( eshagdar ): allow running with params. If ran with 'False', dialogs ( and re-enabling assistve devices ) is suppressed.
	2017-10-18 ( eshagdar ): debugMode is a property. htcLib scriptName is 'htcLib', not 'main.scpt'.
	2017-10-06 ( eshagdar ): added library folder to skip when generating htcLib. renamed variables for clarity.
	2017-09-12 ( eshagdar ): attempt to de-select and re-select the htcLib checkbox.
	2017-06-29 ( eshagdar ): check to see if htcLib exists.
	2017-06-26 ( eshagdar ): quit the app before deleting it.
	2017-06-14 ( eshagdar ): also make an app.
	2016-04-21 ( eshagdar ): Updated clickCommandPosix path.
	2016-03-17 ( eshagdar ): Added DebugMode and clickCommandPosix properties.
	2016-03-15 ( eshagdar ): added documentation to main script. Separated individual functions into library directories and updated run code to loop over files in sub-directories.
	2016-03-14 ( eshagdar ): first created.
*)

property debugMode : true

property LF : ASCII character 10
property tempFileName : "temp.applescript"

property mainFileName : "main.scpt"
property appName : "htcLib"
property appExtension : ".app"
property compiledFolderName : "CompiledHandlers"
property securityPrefPanePosix : "/System/Library/PreferencePanes/Security.prefPane"

on run prefs
	set defaultPrefs to {true}
	if class of prefs is script or prefs is equal to {} then set prefs to defaultPrefs
	set showDialogs to ((item 1 of prefs) as boolean)

	-- now, despite possibly expecting to build app, do NOT actually finalize that if no changes made:
	set appChangesMade to false -- this will be changed to TRUE if any changes are made to handler names and/or params

	set folderName_library to "library:"
	set folderName_toSkip to "standalone"
	set fileNamePrefix_toSkip to "WIP_"

	set tempCode to ""
	set commentBreaker to "--------------------"
	set codeStart to commentBreaker & LF & "-- START OF CODE" & LF & commentBreaker
	set codeEnd to commentBreaker & LF & "-- END OF CODE" & LF & commentBreaker

	-- substrings for parsing sub-handler code:
	set subStr_handlerProperties to "property debugMode: false" & LF & "property ScriptName: \"" & appName & "\"" & LF
	set subStr_beforeHandlerName to LF & "on "
	set subStr_beforeHandlerParams to "("
	set subStr_endPrefixHandler to LF & "end "

	set subStr_templateHandlerWrapperCode to
		"	tell application \"Finder\" to set handlerPath to " &
		"((container of (path to me)) as text) & \"###HANDLER_NAME###\" & \".scpt\"" & LF &
		"	tell (load script alias handlerPath) to return ###HANDLER_CALL###"

	set subStr_special_clickAtCoords_InternalCode_InsertAfter to (ASCII character 9) & "try" & LF
	set subStr_special_clickAtCoords_InternalCode_ToAdd to
		"tell application \"Finder\" to set clickCommandPosix to POSIX path of (((container of container of (path to me)) as string) & \"vendor:cliclick:cliclick\")" & LF
	set subStr_beforeHandlerVersion to "-- version "
	set subStr_afterHandlerVersion to return

	set subStr_beforeCompiledHandlerName to return & "on "

	tell application "Finder"
		set pathRoot to (folder of (path to me)) as string
		set thisFileName to name of (path to me)
	end tell

	set pathTempCode to pathRoot & tempFileName
	set pathMain to pathRoot & mainFileName
	set pathLibrary to pathRoot & folderName_library
	set pathCompiledFolder to pathRoot & compiledFolderName & ":"
	set pathApp to pathCompiledFolder & appName & appExtension

	set libraryNames to list folder (pathLibrary) without invisibles

	-- loop over each sub-directory, compiling each handler and adding to htcLib APP code:
	repeat with dirCount from 1 to count of libraryNames
		set oneLibraryName to item dirCount of libraryNames
		set pathOneLibrary to pathLibrary & oneLibraryName
		if oneLibraryName is equal to folderName_toSkip then
			-- skip this folder because it contains standalone handlers
		else
			-- get all files in the the folder
			set fileNamesInOneLibrary to list folder (pathOneLibrary) without invisibles

			-- append all libraries into a single file
			repeat with fileCount from 1 to count of fileNamesInOneLibrary

				-- reset variables for one file:
				set existingVersion to ""
				set existingHandlerCall to ""
				set shouldCompileHandler to true

				-- read one file and get everything above the helper methods
				set oneFileName to item fileCount of fileNamesInOneLibrary
				if oneFileName does not start with fileNamePrefix_toSkip then
					set pathOneFileinOneLibrary to pathOneLibrary & ":" & oneFileName
					set compiledFileName to replaceSimple({oneFileName, ".applescript", ".scpt"})
					set pathCompiled to (pathCompiledFolder & compiledFileName)

					try
						set existingCompiledCode to (do shell script "osadecompile " & POSIX path of pathCompiled)
						set existingVersion to getTextBetween({existingCompiledCode, subStr_beforeHandlerVersion, subStr_afterHandlerVersion})
						set existingHandlerCall to getTextBetween({existingCompiledCode, subStr_beforeCompiledHandlerName, return})
					end try

					set oneFileRawCode to read file pathOneFileinOneLibrary
					if oneFileRawCode contains codeStart and oneFileRawCode contains codeEnd then
						set codeOneHandler to getTextBetween({oneFileRawCode, codeStart, codeEnd})
					else
						set codeOneHandler to oneFileRawCode
					end if

					set handlerCall to getTextBetween({codeOneHandler, subStr_beforeHandlerName, LF})
					if handlerCall contains subStr_beforeHandlerParams then
						set handlerName to getTextBefore(handlerCall, "(")
					else
						set handlerName to handlerCall
					end if

					set handlerVersion to getTextBetween({codeOneHandler, subStr_beforeHandlerVersion, LF})

					if handlerName is "clickAtCoords" then
						-- SPECIAL MODIFICATION to bring a "property" into the actual handler code.
						set codeOneHandler to my replaceSimple({codeOneHandler, subStr_special_clickAtCoords_InternalCode_InsertAfter, subStr_special_clickAtCoords_InternalCode_InsertAfter & subStr_special_clickAtCoords_InternalCode_ToAdd})
					end if

					-- COMPARE existing (previously-compiled) version and handlerCall with NEW:
					-- if no existing or different, then DO need to re-compile app
					if (existingHandlerCall is equal to handlerCall) and (existingVersion is equal to handlerVersion) then
						-- do NOT need to compile, and no "change" (for this ONE handler!) to the app.
						set shouldCompileHandler to false
					else if (existingHandlerCall is equal to handlerCall) then
						-- do NOT need to re-compile the app due to THIS ONE handler (might need for some other).
					else
						-- the handlerCall was different, so THIS handler requires an app re-compile:
						set appChangesMade to true
					end if

					-- APPEND TO APP: now append to htcLib APP code:
					-- do this EVEN if the handler itself doesn't need to be re-compiled.
					set handlerInternalCode to replaceSimple({replaceSimple({subStr_templateHandlerWrapperCode, "###HANDLER_NAME###", handlerName}), "###HANDLER_CALL###", handlerCall})
					set codeHandlerWrapper to subStr_beforeHandlerName & handlerCall & LF & handlerInternalCode & subStr_endPrefixHandler & handlerName & LF

					if (length of tempCode) is greater than 0 then set tempCode to tempCode & return & return & return
					set tempCode to tempCode & codeHandlerWrapper

					if shouldCompileHandler then
						-- SUBHANDLERS: process each sub-handler this handler depends upon:

						set codeSubHandlers to getTextAfter(oneFileRawCode, codeEnd)

						-- trim the Handler code now (NOT above, since it needs the leading linefeed before the handler call):
						set codeOneHandler to trimWhitespace(codeOneHandler)

						set codeHandlerWithNewSubCalls to subStr_handlerProperties & codeOneHandler
						repeat while codeSubHandlers contains subStr_beforeHandlerName
							set skipThisSubhandler to false
							set oneSubhandlerCall to getTextBetween({codeSubHandlers, subStr_beforeHandlerName, LF})
							if oneSubhandlerCall contains subStr_beforeHandlerParams then
								set oneSubhandlerName to getTextBefore(oneSubhandlerCall, "(")
							else
								set oneSubhandlerName to oneSubhandlerCall
							end if

							if oneSubhandlerName is "coerceToString" then
								if codeOneHandler does not contain "coerceToString" then
									-- the coerceToString was NOT used in the main handler, so we don't actually NEED it:
									-- so SKIP this:
									set skipThisSubhandler to true
								end if
							end if

							if not skipThisSubhandler then
								-- we DO need this subhandler, so reformat it:
								set subHandlerInternalCode to replaceSimple({replaceSimple({subStr_templateHandlerWrapperCode, "###HANDLER_NAME###", oneSubhandlerName}), "###HANDLER_CALL###", oneSubhandlerCall})
								set oneSubHandlerCode to subStr_beforeHandlerName & oneSubhandlerCall & LF & subHandlerInternalCode & subStr_endPrefixHandler & oneSubhandlerName & LF

								set codeHandlerWithNewSubCalls to codeHandlerWithNewSubCalls & LF & oneSubHandlerCode
							end if

							set codeSubHandlers to getTextAfter(codeSubHandlers, subStr_endPrefixHandler & oneSubhandlerName)

						end repeat

						-- write the modified code into a temporary file:
						try
							close access fileRef
						end try
						set fileRef to (open for access file pathTempCode with write permission)
						set eof of fileRef to 0 --> empty file contents if needed
						write codeHandlerWithNewSubCalls to fileRef
						close access fileRef

						-- COMPILE the temporary file to the desired destination:
						do shell script "osacompile -o " & quoted form of POSIX path of pathCompiled & " " & quoted form of POSIX path of pathTempCode
					end if

				end if
			end repeat
		end if
	end repeat

	if appChangesMade then
		-- prepend code with documentation
		set docCode to "-- main script"
		set docCode to docCode & LF & "-- Erik Shagdar, NYHTC"
		set docCode to docCode & LF
		set docCode to docCode & LF & "-- Generated: " & (do shell script "date '+%Y-%m-%d %T'")
		set docCode to docCode & LF & "-- Run " & quoted form of thisFileName & " after making changes in any .applescript file and after each git pull. That script will determine which functions need to be recompiled, as well as whether this app needs to be recompiled."
		set docCode to docCode & LF

		set tempCode to docCode & LF & LF & LF & tempCode

		try
			close access fileRef
		end try
		set fileRef to (open for access file pathTempCode with write permission)
		set eof of fileRef to 0 --> empty file contents if needed
		write tempCode to fileRef
		close access fileRef

		do shell script "osacompile -o " & quoted form of POSIX path of pathMain & " " & quoted form of POSIX path of pathTempCode
		do shell script "pwd"
		do shell script "sh " & quoted form of (POSIX path of (pathRoot & "vendor.sh"))
		if result does not contain "SUCCESS" then return "Error: unable to run vendor.sh"

		set appExists to false
		tell application "Finder"
			if exists pathApp then set appExists to true
		end tell

		--quit the pre-existing app
		if appExists then
			tell application appName to quit
		end if

		-- remove pre-existing app file
		tell application "Finder"
			if appExists then delete file pathApp
		end tell

		-- now make it into an app
		do shell script "osacompile -s -o " & quoted form of POSIX path of pathApp & " " & quoted form of POSIX path of pathTempCode

		if showDialogs then
			tell it to activate
			set AsstAccessDlg to display dialog "You must enable assistive access for " & appName & "." with title appName buttons {"Open", "OK"} default button "OK"

			-- navigate to security preference pane
			if button returned of AsstAccessDlg is equal to "Open" then

				tell application "System Preferences" to activate
				do shell script "open " & quoted form of securityPrefPanePosix

				tell application "System Events"
					tell process "System Preferences"
						click radio button "Privacy" of tab group 1 of window 1

						-- get htcLib checkbox
						set htcLibRow to (first row of table 1 of scroll area 1 of group 1 of tab group 1 of window 1 whose value of static text 1 of UI element 1 contains "htcLib")
						set htcLibCheckbox to checkbox 1 of UI element 1 of htcLibRow
						select htcLibRow

						-- unlock if needed
						set canMakeChanges to enabled of htcLibCheckbox
						if canMakeChanges is false then
							click button 1 of window 1
							display dialog "You must deselect, then reselect the " & appName & " checkbox" buttons "OK" default button "OK"
							return true
						end if

						-- uncheck, then recheck to re-allow control of htcLib
						click htcLibCheckbox
						delay 0.5
						click htcLibCheckbox
						delay 0.5
						set visible to false
					end tell
				end tell

				tell me to activate
				display dialog appName & " is ready" buttons "OK" default button "OK"
			end if
			return true
		else

			tell application "System Preferences" to activate
			do shell script "open " & quoted form of securityPrefPanePosix

			return "you must re-allow assistive devices to '" & appName & "'."

		end if

	end if -- should build APP.

	-- remove the temp file
	tell application "Finder"
		if exists pathTempCode then delete file pathTempCode
	end tell

end run

--------------------
-- HELPER FUNCTIONS
--------------------

on parentFolderOfPath(incomingPath)
	--version 1.1, Daniel A. Shockley

	set incomingPath to incomingPath as string
	if incomingPath is "" then
		error "Cannot find a parent folder for a path that is blank." number -1027
	end if

	if last character of incomingPath is ":" then
		-- if it ends with ":" (a folder), leave that off for the code below
		set incomingPath to (text 1 through -2 of incomingPath) as string
	end if

	set {od, AppleScript's text item delimiters} to {AppleScript's text item delimiters, ":"}
	if (count of text items of incomingPath) is 1 then
		-- the item in question is a disk, so CANNOT return a parent folder
		set AppleScript's text item delimiters to od
		error "Cannot find a parent folder of a DISK: " & incomingPath & "." number -1027
	else
		set enclosingFolder to (text items 1 through -2 of incomingPath as string)
	end if

	set AppleScript's text item delimiters to od
	return enclosingFolder & ":"

end parentFolderOfPath

on getTextBetween(prefs)
	-- version 1.6, Daniel A. Shockley <http://www.danshockley.com>

	set defaultPrefs to {textItemNum:2, includeMarkers:false}

	if (class of prefs is not list) and ((class of prefs) as string is not "record") then
		error "getTextBetween FAILED: parameter should be a record or list. If it is multiple items, just make it into a list to upgrade to this handler." number 1024
	end if
	if class of prefs is list then
		if (count of prefs) is 4 then
			set textItemNum of defaultPrefs to item 4 of prefs
		end if
		set prefs to {sourceTEXT:item 1 of prefs, beforeText:item 2 of prefs, afterText:item 3 of prefs}
	end if
	set prefs to prefs & defaultPrefs -- add on default preferences, if needed
	set sourceTEXT to sourceTEXT of prefs
	set beforeText to beforeText of prefs
	set afterText to afterText of prefs
	set textItemNum to textItemNum of prefs
	set includeMarkers to includeMarkers of prefs

	try
		set {oldDelims, AppleScript's text item delimiters} to {AppleScript's text item delimiters, beforeText}
		set the prefixRemoved to text item textItemNum of sourceTEXT
		set AppleScript's text item delimiters to afterText
		set the finalResult to text item 1 of prefixRemoved
		set AppleScript's text item delimiters to oldDelims

		if includeMarkers then set finalResult to beforeText & finalResult & afterText

	on error errMsg number errNum
		set AppleScript's text item delimiters to oldDelims
		-- 	tell me to log "Error in getTextBetween() : " & errMsg
		set the finalResult to "" -- return nothing if the surrounding text is not found
	end try

	return finalResult

end getTextBetween

on trimWhitespace(inputString)
	-- version 1.2:

	set whiteSpaceAsciiNumbers to {13, 10, 32, 9} -- characters that count as whitespace.

	set textLength to length of inputString
	if textLength is 0 then return ""
	set endSpot to -textLength -- if only whitespace is found, will chop whole string

	-- chop from end
	set i to -1
	repeat while -i is less than or equal to textLength
		set testChar to text i thru i of inputString
		if whiteSpaceAsciiNumbers does not contain (ASCII number testChar) then
			set endSpot to i
			exit repeat
		end if
		set i to i - 1
	end repeat

	if -endSpot is equal to textLength then
		if whiteSpaceAsciiNumbers contains (ASCII number testChar) then return ""
	end if

	set inputString to text 1 thru endSpot of inputString
	set textLength to length of inputString
	set newStart to 1

	-- chop from beginning
	set i to 1
	repeat while i is less than or equal to textLength
		set testChar to text i thru i of inputString
		if whiteSpaceAsciiNumbers does not contain (ASCII number testChar) then
			set newStart to i
			exit repeat
		end if
		set i to i + 1
	end repeat

	set inputString to text newStart thru textLength of inputString

	return inputString

end trimWhitespace

on getTextAfter(sourceTEXT, afterThis)
	-- version 1.2

	try
		set {oldDelims, AppleScript's text item delimiters} to {AppleScript's text item delimiters, {afterThis}}

		if (count of text items of sourceTEXT) is 1 then
			-- the split-string didn't appear at all
			set AppleScript's text item delimiters to oldDelims
			return ""
		else
			set the resultAsList to text items 2 thru -1 of sourceTEXT
		end if
		set AppleScript's text item delimiters to {afterThis}
		set finalResult to resultAsList as string
		set AppleScript's text item delimiters to oldDelims
		return finalResult
	on error errMsg number errNum
		set AppleScript's text item delimiters to oldDelims
		return "" -- return nothing if the stop text is not found
	end try
end getTextAfter

on getTextBefore(sourceTEXT, stopHere)
	-- version 1.1

	try
		set {oldDelims, AppleScript's text item delimiters} to {AppleScript's text item delimiters, stopHere}
		if (count of text items of sourceTEXT) is 1 then
			set AppleScript's text item delimiters to oldDelims
			return ""
		else
			set the finalResult to text item 1 of sourceTEXT
		end if
		set AppleScript's text item delimiters to oldDelims
		return finalResult
	on error errMsg number errNum
		set AppleScript's text item delimiters to oldDelims
		return "" -- return nothing if the stop text is not found
	end try
end getTextBefore

on replaceSimple(prefs)
	-- version 1.4

	set defaultPrefs to {considerCase:true}

	if class of prefs is list then
		if (count of prefs) is greater than 3 then
			-- get any parameters after the initial 3
			set prefs to {sourceTEXT:item 1 of prefs, oldChars:item 2 of prefs, newChars:item 3 of prefs, considerCase:item 4 of prefs}
		else
			set prefs to {sourceTEXT:item 1 of prefs, oldChars:item 2 of prefs, newChars:item 3 of prefs}
		end if

	else if class of prefs is not equal to (class of {someKey:3}) then
		-- Test by matching class to something that IS a record to avoid FileMaker namespace conflict with the term "record"

		error "The parameter for 'replaceSimple()' should be a record or at least a list. Wrap the parameter(s) in curly brackets for easy upgrade to 'replaceSimple() version 1.3. " number 1024

	end if

	set prefs to prefs & defaultPrefs

	set considerCase to considerCase of prefs
	set sourceTEXT to sourceTEXT of prefs
	set oldChars to oldChars of prefs
	set newChars to newChars of prefs

	set sourceTEXT to sourceTEXT as string

	set oldDelims to AppleScript's text item delimiters
	set AppleScript's text item delimiters to the oldChars
	if considerCase then
		considering case
			set the parsedList to every text item of sourceTEXT
			set AppleScript's text item delimiters to the {(newChars as string)}
			set the newText to the parsedList as string
		end considering
	else
		ignoring case
			set the parsedList to every text item of sourceTEXT
			set AppleScript's text item delimiters to the {(newChars as string)}
			set the newText to the parsedList as string
		end ignoring
	end if
	set AppleScript's text item delimiters to oldDelims
	return newText

end replaceSimple


Add custom list dialog titles and messages

This restores previous behavior that was broken with the recent list
dialog modifications (splitting the dialogs into top categories,
subcategories and all categories instead of just the one full category
list).

(*
	Log Page - Log categorized web page bookmarks to a text file

	Version: @@VERSION@@
	Date:    2012-12-20
	Author:  Steve Wheeler

	Get the title, URL, current date and time, and a user-definable
	category for the frontmost Safari window and log the info to a text
	file.

	This program is free software available under the terms of a
	BSD-style (3-clause) open source license detailed below.
*)

(*
Copyright (c) 2011-2012 Steve Wheeler
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

* Redistributions of source code must retain the above copyright notice, this
  list of conditions and the following disclaimer.

* Redistributions in binary form must reproduce the above copyright notice,
  this list of conditions and the following disclaimer in the documentation
  and/or other materials provided with the distribution.

* Neither the name of the copyright holder nor the names of
  contributors may be used to endorse or promote products derived from
  this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*)

(*
	TODO: Change the following local-only config before any public release.
	The log file should be: $GTD_IN/logs/urls/urls.YYYY.txt
	Or just: $GTD_IN/logs/urls.txt

	TODO: The edit_log() function is currently incomplete. It will only launch
	an editor from the shell and only if that editor is a GUI editor (in this
	case, MacVim). If I release this script publicly, I will still need to
	implement funtionality to launch a GUI editor normally (with the 'open'
	AppleScript commmand) as well as a command-line editor run in the Terminal.
*)

property script_name : "Log Page"
property script_version : "@@VERSION@@"

property uRule : data utxt2500 as Unicode text -- BOX DRAWINGS LIGHT HORIZONTAL

--property p_list_types : {"top", "sub", "all"}

global g_top_categories, g_all_categories, g_previous_categories
global g_list_type, g_previous_list_type
global g_prompt_count, g_prompt_total

set g_list_type to missing value
set g_previous_list_type to missing value

---------- USER CONFIGURATION ----------------------------------------
-- Default log directory to use if one is not read from config file:
--set log_dir to POSIX path of (path to application support folder from user domain) & "Jazzhead/Log Page/"
set log_dir to POSIX path of (path to desktop folder) & "Log Page/" -- :DEBUG:

-- Log file name:
set log_name to "urls.txt"

-- Default categories/labels if none are found in (or there is no)
-- URL file. Modify as desired:
set default_cats to "Development
Development:AppleScript
Development:AppleScript:Mail
Design
Productivity:GTD
Apple:Mac OS X
Apple:iOS
Career
Education
Health
Health:Diet
Health:Fitness
General"

-- Text editor to use if manually editing the log file.
--
-- This example:
--	1. Opens the file using MacVim ('mvim' as installed by MacPorts)
--	2. Switches the local working directory for just that buffer to the
--	    directory containing the file ('lcd')
--	3. Gives the MacVim window a name ('--servername').
--
set text_editor to "/opt/local/bin/mvim \"+lcd %:p:h\" --servername LOG_PAGE"

-- Should the specified editor be launched from the unix shell?
set should_use_shell to true
---------- END User Configuration ----------------------------------------

---------- ADVANCED (EXPERIMENTAL) ---------------------------------------------
-- For advanced users familiar with the command-line, override the default log
-- directory path. This probably won't be in a public release.
--
-- Grab an exported shell environment variable from a config file to use as the
-- log directory path. For this example, I have a "~/.gtdrc" config file that I
-- source from my "~/.bash_profile" file so that it exportss environment
-- variables relating to my GTD system. That makes those variables available to
-- any of the shell scripts and tools that I use. For instance, when I'm
-- editing files in Vim, I can write paths to other files using one of the
-- environment variables rather than a full or relative path and the 'gf'
-- command will work for jumping to that file. The config file looks like this:
--
--         # ~/.gtdrc
--         #
--         # File locations for GTD documents
--         #
--         export GTD_DIR=$HOME/Documents/org  # GTD root directory
--         export GTD_IN=$GTD_DIR/content      # source/input files
--         export GTD_OUT=$GTD_DIR/html        # generated HTML output files
--         export GTD_LIB=$GTD_DIR/lib         # code library files
--         export GTD_BIN=$GTD_LIB/script      # scripts
--
-- Remove (or comment out) the "(*" and "*)" comment markers below to use this
-- section. Make sure you also configure the two variables for your system.
--(*
--------------- USER CONFIGURATION ----------
-- Path to your config file:
set cfg_file to POSIX path of (path to home folder) & ".gtdrc"
-- The environment variable you're interested in:
set env_var to "GTD_IN"
--------------- END User Configuration ---------

-- Check for environment variable in config file:
set s to "egrep '\\<" & env_var & "=' " & quoted form of cfg_file &
	" > /dev/null 2>&1; echo $?"
set shell_status to (do shell script s) as integer
-- Source the config file and echo the desired variable:
if shell_status is 0 then -- (means that grep found a match)
	set s to ". " & quoted form of cfg_file & " && echo $" & env_var
	set gtd_in_dir to do shell script s
	set log_dir to gtd_in_dir & "/logs/"

	--------------------
	-- OPTIONAL: Override the Text editor to integrate with my GTD system.
	--
	-- This example:
	--	1. Opens the file using MacVim ('mvim' as installed by MacPorts)
	--	2. Switches the local working directory ('lcd') for just that buffer to
	--	    my GTD content directory ($GTD_IN after escaping potential spaces)
	--	3. Gives the MacVim window a name ('--servername').

	----- First, escape any potential spaces in the directory path:
	--
	--set gtd_in_dir to gtd_in_dir & " foo    bar" -- :DEBUG: spaces in path
	--
	set s to "echo " & quoted form of gtd_in_dir & " | sed 's/ \\{1,\\}/\\\\ /g'" -- escape spaces
	set gtd_in_dir to do shell script s
	--
	--log gtd_in_dir -- :DEBUG: need to use 'log' to see the value w/o AppleScript escapes

	----- Finally, set the editor with all of the options:
	--
	set text_editor to "/opt/local/bin/mvim \"+lcd " & gtd_in_dir & "\" --servername GTD --remote-silent"
	--
	--log text_editor -- :DEBUG:
	--return text_editor -- :DEBUG:
	--------------------
end if
---------- END Advanced --------------------------------------------------------
--*)

--return log_dir -- :DEBUG:

--
-- Get URL and title from front browser window
--
tell application "Safari"
	--activate
	try
		set this_url to URL of front document
		set this_title to name of first tab of front window whose visible is true
	on error
		return false
	end try
end tell
-- Transliterate non-ASCII characters to ASCII
set this_title to convert_to_ascii(this_title)

--return this_title -- :DEBUG:

--
-- Format the record separator between log entries
--
set rule_char to "-"
set rule_width to 80
set field_width to 7
set field_sep to " | "
set rec_sep to "" & multiply_text(rule_char, field_width - 1) &
	"+" & multiply_text(rule_char, rule_width - field_width)

--
-- Format date-time string as "YYYY-MM-DD HH:MM:SS"
--
set {year:y, month:m, day:d, hours:hh, minutes:mm, seconds:ss} to current date
set m to m as integer -- coerce month string
-- Pad numbers with leading zeros:
set tmp_list to {}
repeat with this_item in {m, d, hh, mm, ss}
	set this_item to text 2 thru -1 of (100 + this_item as text)
	set end of tmp_list to this_item
end repeat
set {m, d, hh, mm, ss} to tmp_list
-- Final string:
set date_time to join_list({y, m, d}, "-") & " " & join_list({hh, mm, ss}, ":")

-- Log file name
--set log_name to "urls." & y & ".txt"

--
-- Create log directory if it doesn't already exist. (Go ahead and use a shell
-- command since it's easy to create intermediate directories all in one step.):
--
try
	set s to "mkdir -pm700 " & quoted form of log_dir
	do shell script s
on error err_msg number err_num
	error "Can't create log directory: " & err_msg number err_num
	return "Fatal Error: Can't create log directory"
end try

set log_file_posix to "" & log_dir & log_name
set log_file to POSIX file log_file_posix

--
-- Add header to new or empty log file
--
set should_init to false
tell application "Finder"
	if (not (exists log_file)) or (_IO's read_file(log_file) is "") then
		set should_init to true
	end if
end tell

set head_sep to multiply_text("#", 80)
if should_init then
	set file_header to head_sep & "
# urls.txt - Timestamped and categorized URL archive
# ==================================================
# Optionally list some categories/labels of URLs below that might not yet
# be used. Any categories/lables listed here or in the web page records
# will be presented as a list to select from when saving new URLs.
" & head_sep & linefeed & default_categories & linefeed & head_sep & "
# END: category/label list
" & head_sep & linefeed
	tell _IO to write_file(log_file, file_header)
	set all_category_txt to default_categories
else
	-- Read categories from file
	(*
		:TODO: Error handling.
	*)
	set all_category_txt to text 2 thru -2 of (item 3 of split_text(_IO's read_file(log_file), head_sep))
end if

-- Parse any existing labels from the "Label" fields of the URLs file:
set s to "LANG=C sed -n 's/^Label | //p' " & quoted form of log_file_posix & " | sort | uniq"
--return s
set categories_used to do shell script s without altering line endings
-- Sort those along with the manually entered categories/labels:
set s to "echo \"" & all_category_txt & linefeed & categories_used & "\" | egrep -v '^$' | sort | uniq"
set all_category_txt to do shell script s without altering line endings
--return all_category_txt -- :DEBUG:
-- Coerce the lines into a list:
set g_all_categories to paragraphs of all_category_txt
if g_all_categories's last item is "" then set g_all_categories to g_all_categories's items 1 thru -2
--return g_all_categories -- :DEBUG:

-- Get top-level categories:
set s to "LANG=C echo \"" & all_category_txt & "\" | sed -n 's/^\\([^:]\\{1,\\}\\).*/\\1/p' | uniq"
set top_category_txt to do shell script s without altering line endings
--return top_category_txt -- :DEBUG:
-- Coerce the lines into a list:
set g_top_categories to paragraphs of top_category_txt
if g_top_categories's last item is "" then set g_top_categories to g_top_categories's items 1 thru -2
--return g_top_categories -- :DEBUG:

--
-- Prompt for title, category/subcategories and optional note for URL
--

-- The number of dialog prompts will vary depending on if a subcategory list is requested.
set g_prompt_count to 1 -- increment with every dialog or list prompt, decrement if going back
set g_prompt_total to 4 -- increment if sub/full lists are displayed, decrement if going back

-- Accept or modify URL title
set b to {"Cancel", "Manually Edit Log File", "Next..."}
set t to "" & script_name & ": Title (" & g_prompt_count & "/" & g_prompt_total & ")"
set m to "To log the URL for:" & return & return
	& tab & "\"" & this_title & "\"" & return & return &
	"first accept or edit the title."
display dialog m default answer this_title with title t buttons b default button last item of b
set {this_title, btn_pressed} to {text returned of result, button returned of result}
set g_prompt_count to g_prompt_count + 1
if btn_pressed is item 2 of b then
	if should_use_shell then
		set this_log_file to quoted form of log_file_posix
	else
		set this_log_file to log_file
	end if
	edit_log(this_log_file, text_editor, should_use_shell)
	return "Script ended with '" & (item 2 of b as text) & "'"
end if

-- Select a category
set chosen_category to choose_category(g_top_categories, "top")
if chosen_category is false then error number -128 -- User canceled

-- Modify selected category or enter a new category
set t to "" & script_name & ": Category (" & g_prompt_count & "/" & g_prompt_total & ")"
set m to "Please provide a category and any optional subcategories (or edit your selected category) for the URL. Example: \"Development:AppleScript:Mail\""
display dialog m default answer chosen_category with title t buttons b default button last item of b
set {this_label, btn_pressed} to {text returned of result, button returned of result}
set g_prompt_count to g_prompt_count + 1

-- Optionally add note
if btn_pressed is last item of b then
	set last item of b to "Append URL to Log File"
	set t to "" & script_name & ": Note (" & g_prompt_count & "/" & g_prompt_total & ")"
	set m to "Optionally add a short note. Just leave the field blank if you don't want to add a note."
	display dialog m default answer "" with title t buttons b default button last item of b
	set {this_note, btn_pressed} to {text returned of result, button returned of result}
end if

--
-- Append or edit the log file
--
if btn_pressed is last item of b then
	set final_text to join_list({
		"Date " & field_sep & date_time,
		"Label" & field_sep & this_label,
		"Title" & field_sep & this_title,
		"URL  " & field_sep & this_url}, linefeed) & linefeed
	if this_note is not "" then
		set final_text to final_text & "Note " & field_sep & this_note & linefeed
	end if
	set final_text to final_text & rec_sep & linefeed
	--return "[DEBUG]" & return & final_text
	_IO's append_file(log_file, final_text)
else
	if should_use_shell then
		set this_log_file to quoted form of log_file_posix
	else
		set this_log_file to log_file
	end if
	edit_log(this_log_file, text_editor, should_use_shell)
end if

--display alert "Log Page" message "DEBUG: Script complete."

(************************************************************
 *	Subroutines
 ************************************************************)

-- ===== Main Functions =====

----- Dialog Views

on choose_category(cur_list, cur_list_type)
	--log "[debug] in choose_category(_list_, \"" & cur_list_type & "\")"

	if cur_list_type is not "all" then copy cur_list to g_previous_categories

	--log "[debug] g_previous_categories: " & joinList(g_previous_categories, ", ")

	set_list_type(cur_list_type)

	set extra_items to get_extra_items(cur_list_type)
	set list_rule to extra_items's last item

	-- Customize list dialog properties
	set t to "" & script_name & ": Category (" & g_prompt_count & "/" & g_prompt_total & ")"
	if cur_list_type is "top" then
		set m to "Please select a top-level category for the URL you want to log. Next you will be able to select subcategories."
	else if cur_list_type is in {"sub", "all"} then
		set m to "Please select a category or subcategory for the URL you want to log. You will have a chance to edit your choice (to add a new category or subcategory)."
	end if
	set b to "Next..."

	repeat 10 times -- limit loops as a precaution
		set chosen_category to choose from list extra_items & cur_list with title t with prompt m OK button name b
		if chosen_category as text is not list_rule then exit repeat
	end repeat
	if chosen_category is false then return false

	-- Call this handler recursively if another list was requested:
	if cur_list_type is in {"top", "sub"} and chosen_category as text is extra_items's first item then
		--log "[debug] incrementing both prompt count and total"
		set g_prompt_count to g_prompt_count + 1
		set g_prompt_total to g_prompt_total + 1
		--log "[debug] recurse to choose_category(g_all_categories, \"all\")"
		set chosen_category to choose_category(g_all_categories, "all")
	else if cur_list_type is "sub" and chosen_category as text is extra_items's second item then
		--log "[debug] decrementing both prompt count and total"
		set g_prompt_count to g_prompt_count - 1
		set g_prompt_total to g_prompt_total - 1
		--log "[debug] recurse to choose_category(g_top_categories, \"top\")"
		set chosen_category to choose_category(g_top_categories, "top")
	else if cur_list_type is "all" and chosen_category as text is extra_items's first item then
		--log "[debug] decrementing both prompt count and total"
		set g_prompt_count to g_prompt_count - 1
		set g_prompt_total to g_prompt_total - 1
		if g_previous_list_type is "sub" then
			--log "[debug] recurse to choose_category(g_previous_categories, \"sub\")"
			set chosen_category to choose_category(g_previous_categories, "sub")
		else if g_previous_list_type is "top" then
			--log "[debug] recurse to choose_category(g_top_categories, \"top\")"
			set chosen_category to choose_category(g_top_categories, "top")
		end if
	else if cur_list_type is "top" then
		set sub_categories to get_subcategories(chosen_category)
		--log "[debug] incrementing both prompt count and total"
		set g_prompt_count to g_prompt_count + 1
		set g_prompt_total to g_prompt_total + 1
		--log "[debug] recurse to choose_category(sub_categories, \"sub\")"
		set chosen_category to choose_category(sub_categories, "sub")
	else
		--log "[debug] incrementing prompt count"
		set g_prompt_count to g_prompt_count + 1
	end if

	return chosen_category
end choose_category

on get_subcategories(chosen_category)
	set sub_categories to {}
	repeat with this_cat in g_all_categories
		if (this_cat as text) is (chosen_category as text)
			or (this_cat as text) starts with (chosen_category & ":") then
			set end of sub_categories to this_cat as text
		end if
	end repeat
	return sub_categories
end get_subcategories

on set_list_type(cur_type)
	-- :TODO: verify that 'cur_type' is in 'p_list_types'

	set g_previous_list_type to g_list_type
	set g_list_type to cur_type
end set_list_type

on get_extra_items(list_type)
	if list_type is "top" then

		-- :TODO: Add choice to make a new category, bypassing subcategory dialog

		return {"Show full list with subcategories...", multiplyTxt(uRule, 20)}
	else if list_type is "sub" then
		return {"Show full list of categories...", "Go back to previous list...", multiplyTxt(uRule, 35)}
	else if list_type is "all" then
		return {"Go back to previous list...", multiplyTxt(uRule, 35)}
	else
		return false
	end if
end get_extra_items

----- External Views

on edit_log(log_file, text_editor, should_use_shell)
	(*
		TODO:MAYBE:
		Have a configuration variable in the main script to specify which text
		editor to use. Have another variable for configuring whether or not
		it's a GUI application. Then this function would use 'open' to launch a
		GUI app or script the Terminal to launch a command-line editor.
		Optionally use 'do shell script' to launch a compatible GUI app from a
		command line (so that the working directory can be changed to that of
		the file first), but I don't know if that will really be necessary
		since I have a Vim shortcut for quickly changing the working directory
		if need be.
	*)
	if should_use_shell then
		set s to text_editor & space & log_file & space & "> /dev/null 2>&1"
		do shell script s
		return
	else
		set t to script_name
		set m to ":TODO: edit_log() function: use non-shell-invoked editor."
		set b to {"Cancel"}
		display alert t message m buttons b cancel button 1 as critical
		return false
	end if
end edit_log

-- ===== Utility Functions =====

on convert_to_ascii(non_ascii_txt)
	--
	-- From 'man iconv_open':
	--
	--    When  the string "//TRANSLIT" is appended to tocode, transliteration is
	--    activated. This means that when a character cannot  be  represented  in
	--    the target character set, it can be approximated through one or several
	--    characters that look similar to the original character.
	--
	set s to "iconv -f UTF-8 -t US-ASCII//TRANSLIT <<<" & quoted form of non_ascii_txt
	do shell script s
end convert_to_ascii

on multiply_text(str, n)
	if n < 1 or str = "" then return ""
	set lst to {}
	repeat n times
		set end of lst to str
	end repeat
	return lst as string
end multiply_text

on split_text(txt, delim)
	set old_tids to AppleScript's text item delimiters
	try
		set AppleScript's text item delimiters to (delim as string)
		set lst to every text item of (txt as string)
		set AppleScript's text item delimiters to old_tids
		return lst
	on error err_msg number err_num
		set AppleScript's text item delimiters to old_tids
		error "Can't split_text(): " & err_msg number err_num
	end try
end split_text

on join_list(lst, delim)
	set old_tids to AppleScript's text item delimiters
	try
		set AppleScript's text item delimiters to (delim as string)
		set txt to lst as string
		set AppleScript's text item delimiters to old_tids
		return txt
	on error err_msg number err_num
		set AppleScript's text item delimiters to old_tids
		error "Can't join_list(): " & err_msg number err_num
	end try
end join_list

(************************************************************
 *	Script Objects
 ************************************************************)

script _IO

	(* == PUBLIC == *)

	on write_file(file_path, this_data)
		_write_file(file_path, this_data, string, false) -- overwrite existing file
	end write_file

	on append_file(file_path, this_data)
		_write_file(file_path, this_data, string, true) -- append new data to end of existing file
	end append_file

	on read_file(file_path)
		_read_file(file_path, string)
	end read_file

	(* == PRIVATE == *)

	on _write_file(file_path, this_data, data_class, should_append_data)
		try
			set file_path to file_path as text
			set file_handle to
				open for access file file_path with write permission
			if not should_append_data then
				set eof of the file_handle to 0
			write this_data as data_class to file_handle starting at eof
			close access file_handle
			return true
		on error err_msg number err_num
			try
				close access file file_path
			end try
			error err_msg number err_num
			return false
		end try
	end _write_file

	on _read_file(file_path, data_class)
		try
			set file_path to file_path as text
			if (get eof file file_path) is 0 then
				set file_contents to ""
			else
				set file_contents to read file file_path as data_class
			end if
			return file_contents
		on error
			return 0
		end try
	end _read_file
end script

(*
	Log Page - Log categorized web page bookmarks to a text file

	Version: @@VERSION@@
	Date:    2012-12-20
	Author:  Steve Wheeler

	Get the title, URL, current date and time, and a user-definable
	category for the frontmost Safari window and log the info to a text
	file.

	This program is free software available under the terms of a
	BSD-style (3-clause) open source license detailed below.
*)

(*
Copyright (c) 2011-2012 Steve Wheeler
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

* Redistributions of source code must retain the above copyright notice, this
  list of conditions and the following disclaimer.

* Redistributions in binary form must reproduce the above copyright notice,
  this list of conditions and the following disclaimer in the documentation
  and/or other materials provided with the distribution.

* Neither the name of the copyright holder nor the names of
  contributors may be used to endorse or promote products derived from
  this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*)

(*
	TODO: Change the following local-only config before any public release.
	The log file should be: $GTD_IN/logs/urls/urls.YYYY.txt
	Or just: $GTD_IN/logs/urls.txt

	TODO: The edit_log() function is currently incomplete. It will only launch
	an editor from the shell and only if that editor is a GUI editor (in this
	case, MacVim). If I release this script publicly, I will still need to
	implement funtionality to launch a GUI editor normally (with the 'open'
	AppleScript commmand) as well as a command-line editor run in the Terminal.
*)

property script_name : "Log Page"
property script_version : "@@VERSION@@"

property uRule : data utxt2500 as Unicode text -- BOX DRAWINGS LIGHT HORIZONTAL

--property p_list_types : {"top", "sub", "all"}

global g_top_categories, g_all_categories, g_previous_categories
global g_list_type, g_previous_list_type

set g_list_type to missing value
set g_previous_list_type to missing value

---------- USER CONFIGURATION ----------------------------------------
-- Default log directory to use if one is not read from config file:
--set log_dir to POSIX path of (path to application support folder from user domain) & "Jazzhead/Log Page/"
set log_dir to POSIX path of (path to desktop folder) & "Log Page/" -- :DEBUG:

-- Log file name:
set log_name to "urls.txt"

-- Default categories/labels if none are found in (or there is no)
-- URL file. Modify as desired:
set default_cats to "Development
Development:AppleScript
Development:AppleScript:Mail
Design
Productivity:GTD
Apple:Mac OS X
Apple:iOS
Career
Education
Health
Health:Diet
Health:Fitness
General"

-- Text editor to use if manually editing the log file.
--
-- This example:
--	1. Opens the file using MacVim ('mvim' as installed by MacPorts)
--	2. Switches the local working directory for just that buffer to the
--	    directory containing the file ('lcd')
--	3. Gives the MacVim window a name ('--servername').
--
set text_editor to "/opt/local/bin/mvim \"+lcd %:p:h\" --servername LOG_PAGE"

-- Should the specified editor be launched from the unix shell?
set should_use_shell to true
---------- END User Configuration ----------------------------------------

---------- ADVANCED (EXPERIMENTAL) ---------------------------------------------
-- For advanced users familiar with the command-line, override the default log
-- directory path. This probably won't be in a public release.
--
-- Grab an exported shell environment variable from a config file to use as the
-- log directory path. For this example, I have a "~/.gtdrc" config file that I
-- source from my "~/.bash_profile" file so that it exportss environment
-- variables relating to my GTD system. That makes those variables available to
-- any of the shell scripts and tools that I use. For instance, when I'm
-- editing files in Vim, I can write paths to other files using one of the
-- environment variables rather than a full or relative path and the 'gf'
-- command will work for jumping to that file. The config file looks like this:
--
--         # ~/.gtdrc
--         #
--         # File locations for GTD documents
--         #
--         export GTD_DIR=$HOME/Documents/org  # GTD root directory
--         export GTD_IN=$GTD_DIR/content      # source/input files
--         export GTD_OUT=$GTD_DIR/html        # generated HTML output files
--         export GTD_LIB=$GTD_DIR/lib         # code library files
--         export GTD_BIN=$GTD_LIB/script      # scripts
--
-- Remove (or comment out) the "(*" and "*)" comment markers below to use this
-- section. Make sure you also configure the two variables for your system.
--(*
--------------- USER CONFIGURATION ----------
-- Path to your config file:
set cfg_file to POSIX path of (path to home folder) & ".gtdrc"
-- The environment variable you're interested in:
set env_var to "GTD_IN"
--------------- END User Configuration ---------

-- Check for environment variable in config file:
set s to "egrep '\\<" & env_var & "=' " & quoted form of cfg_file &
	" > /dev/null 2>&1; echo $?"
set shell_status to (do shell script s) as integer
-- Source the config file and echo the desired variable:
if shell_status is 0 then -- (means that grep found a match)
	set s to ". " & quoted form of cfg_file & " && echo $" & env_var
	set gtd_in_dir to do shell script s
	set log_dir to gtd_in_dir & "/logs/"

	--------------------
	-- OPTIONAL: Override the Text editor to integrate with my GTD system.
	--
	-- This example:
	--	1. Opens the file using MacVim ('mvim' as installed by MacPorts)
	--	2. Switches the local working directory ('lcd') for just that buffer to
	--	    my GTD content directory ($GTD_IN after escaping potential spaces)
	--	3. Gives the MacVim window a name ('--servername').

	----- First, escape any potential spaces in the directory path:
	--
	--set gtd_in_dir to gtd_in_dir & " foo    bar" -- :DEBUG: spaces in path
	--
	set s to "echo " & quoted form of gtd_in_dir & " | sed 's/ \\{1,\\}/\\\\ /g'" -- escape spaces
	set gtd_in_dir to do shell script s
	--
	--log gtd_in_dir -- :DEBUG: need to use 'log' to see the value w/o AppleScript escapes

	----- Finally, set the editor with all of the options:
	--
	set text_editor to "/opt/local/bin/mvim \"+lcd " & gtd_in_dir & "\" --servername GTD --remote-silent"
	--
	--log text_editor -- :DEBUG:
	--return text_editor -- :DEBUG:
	--------------------
end if
---------- END Advanced --------------------------------------------------------
--*)

--return log_dir -- :DEBUG:

--
-- Get URL and title from front browser window
--
tell application "Safari"
	--activate
	try
		set this_url to URL of front document
		set this_title to name of first tab of front window whose visible is true
	on error
		return false
	end try
end tell
-- Transliterate non-ASCII characters to ASCII
set this_title to convert_to_ascii(this_title)

--return this_title -- :DEBUG:

--
-- Format the record separator between log entries
--
set rule_char to "-"
set rule_width to 80
set field_width to 7
set field_sep to " | "
set rec_sep to "" & multiply_text(rule_char, field_width - 1) &
	"+" & multiply_text(rule_char, rule_width - field_width)

--
-- Format date-time string as "YYYY-MM-DD HH:MM:SS"
--
set {year:y, month:m, day:d, hours:hh, minutes:mm, seconds:ss} to current date
set m to m as integer -- coerce month string
-- Pad numbers with leading zeros:
set tmp_list to {}
repeat with this_item in {m, d, hh, mm, ss}
	set this_item to text 2 thru -1 of (100 + this_item as text)
	set end of tmp_list to this_item
end repeat
set {m, d, hh, mm, ss} to tmp_list
-- Final string:
set date_time to join_list({y, m, d}, "-") & " " & join_list({hh, mm, ss}, ":")

-- Log file name
--set log_name to "urls." & y & ".txt"

--
-- Create log directory if it doesn't already exist. (Go ahead and use a shell
-- command since it's easy to create intermediate directories all in one step.):
--
try
	set s to "mkdir -pm700 " & quoted form of log_dir
	do shell script s
on error err_msg number err_num
	error "Can't create log directory: " & err_msg number err_num
	return "Fatal Error: Can't create log directory"
end try

set log_file_posix to "" & log_dir & log_name
set log_file to POSIX file log_file_posix

--
-- Add header to new or empty log file
--
set should_init to false
tell application "Finder"
	if (not (exists log_file)) or (_IO's read_file(log_file) is "") then
		set should_init to true
	end if
end tell

set head_sep to multiply_text("#", 80)
if should_init then
	set file_header to head_sep & "
# urls.txt - Timestamped and categorized URL archive
# ==================================================
# Optionally list some categories/labels of URLs below that might not yet
# be used. Any categories/lables listed here or in the web page records
# will be presented as a list to select from when saving new URLs.
" & head_sep & linefeed & default_categories & linefeed & head_sep & "
# END: category/label list
" & head_sep & linefeed
	tell _IO to write_file(log_file, file_header)
	set all_category_txt to default_categories
else
	-- Read categories from file
	(*
		:TODO: Error handling.
	*)
	set all_category_txt to text 2 thru -2 of (item 3 of split_text(_IO's read_file(log_file), head_sep))
end if

-- Parse any existing labels from the "Label" fields of the URLs file:
set s to "LANG=C sed -n 's/^Label | //p' " & quoted form of log_file_posix & " | sort | uniq"
--return s
set categories_used to do shell script s without altering line endings
-- Sort those along with the manually entered categories/labels:
set s to "echo \"" & all_category_txt & linefeed & categories_used & "\" | egrep -v '^$' | sort | uniq"
set all_category_txt to do shell script s without altering line endings
--return all_category_txt -- :DEBUG:
-- Coerce the lines into a list:
set g_all_categories to paragraphs of all_category_txt
if g_all_categories's last item is "" then set g_all_categories to g_all_categories's items 1 thru -2
--return g_all_categories -- :DEBUG:

-- Get top-level categories:
set s to "LANG=C echo \"" & all_category_txt & "\" | sed -n 's/^\\([^:]\\{1,\\}\\).*/\\1/p' | uniq"
set top_category_txt to do shell script s without altering line endings
--return top_category_txt -- :DEBUG:
-- Coerce the lines into a list:
set g_top_categories to paragraphs of top_category_txt
if g_top_categories's last item is "" then set g_top_categories to g_top_categories's items 1 thru -2
--return g_top_categories -- :DEBUG:

--
-- Prompt for title, category/subcategories and optional note for URL
--

-- The number of dialog prompts will vary depending on if a subcategory list is requested.
set prompt_count to 1 -- increment with every dialog or list prompt
set prompt_total to 4 -- increment if the subcategory list is displayed

-- Accept or modify URL title
set b to {"Cancel", "Manually Edit Log File", "Next..."}
set t to "" & script_name & ": Title (" & prompt_count & "/" & prompt_total & ")"
set m to "To log the URL for:" & return & return
	& tab & "\"" & this_title & "\"" & return & return &
	"first accept or edit the title."
display dialog m default answer this_title with title t buttons b default button last item of b
set {this_title, btn_pressed} to {text returned of result, button returned of result}
set prompt_count to prompt_count + 1
if btn_pressed is item 2 of b then
	if should_use_shell then
		set this_log_file to quoted form of log_file_posix
	else
		set this_log_file to log_file
	end if
	edit_log(this_log_file, text_editor, should_use_shell)
	return "Script ended with '" & (item 2 of b as text) & "'"
end if

-- Select a category
set chosen_category to choose_category(g_top_categories, "top")
if chosen_category is false then error number -128 -- User canceled

-- Modify selected category or enter a new category
set t to "" & script_name & ": Category (" & prompt_count & "/" & prompt_total & ")"
set m to "Please provide a category and any optional subcategories (or edit your selected category) for the URL. Example: \"Development:AppleScript:Mail\""
display dialog m default answer chosen_category with title t buttons b default button last item of b
set {this_label, btn_pressed} to {text returned of result, button returned of result}
set prompt_count to prompt_count + 1

-- Optionally add note
if btn_pressed is last item of b then
	set last item of b to "Append URL to Log File"
	set t to "" & script_name & ": Note (" & prompt_count & "/" & prompt_total & ")"
	set m to "Optionally add a short note. Just leave the field blank if you don't want to add a note."
	display dialog m default answer "" with title t buttons b default button last item of b
	set {this_note, btn_pressed} to {text returned of result, button returned of result}
end if

--
-- Append or edit the log file
--
if btn_pressed is last item of b then
	set final_text to join_list({
		"Date " & field_sep & date_time,
		"Label" & field_sep & this_label,
		"Title" & field_sep & this_title,
		"URL  " & field_sep & this_url}, linefeed) & linefeed
	if this_note is not "" then
		set final_text to final_text & "Note " & field_sep & this_note & linefeed
	end if
	set final_text to final_text & rec_sep & linefeed
	--return "[DEBUG]" & return & final_text
	_IO's append_file(log_file, final_text)
else
	if should_use_shell then
		set this_log_file to quoted form of log_file_posix
	else
		set this_log_file to log_file
	end if
	edit_log(this_log_file, text_editor, should_use_shell)
end if

--display alert "Log Page" message "DEBUG: Script complete."

(************************************************************
 *	Subroutines
 ************************************************************)

-- ===== Main Functions =====

on choose_category(cur_list, cur_list_type)
	log "[debug] in choose_category(_list_, \"" & cur_list_type & "\")"

	if cur_list_type is not "all" then copy cur_list to g_previous_categories

	log "[debug] g_previous_categories: " & joinList(g_previous_categories, ", ")

	set_list_type(cur_list_type)

	set extra_items to get_extra_items(cur_list_type)
	set list_rule to extra_items's last item

	-- :TODO: Customize dialog message
	-- :TODO: Customize dialog title with dialog number

	repeat 10 times -- limit loops as a precaution
		set chosen_category to choose from list extra_items & cur_list
		if chosen_category as text is not list_rule then exit repeat
	end repeat
	if chosen_category is false then return false

	-- Call this handler recursively if another list was requested:
	if cur_list_type is in {"top", "sub"} and chosen_category as text is extra_items's first item then
		log "[debug] recurse to choose_category(g_all_categories, \"all\")"
		set chosen_category to choose_category(g_all_categories, "all")
	else if cur_list_type is "sub" and chosen_category as text is extra_items's second item then
		log "[debug] recurse to choose_category(g_top_categories, \"top\")"
		set chosen_category to choose_category(g_top_categories, "top")
	else if cur_list_type is "all" and chosen_category as text is extra_items's first item then
		if g_previous_list_type is "sub" then
			log "[debug] recurse to choose_category(g_previous_categories, \"sub\")"
			set chosen_category to choose_category(g_previous_categories, "sub")
		else if g_previous_list_type is "top" then
			log "[debug] recurse to choose_category(g_top_categories, \"top\")"
			set chosen_category to choose_category(g_top_categories, "top")
		end if
	else if cur_list_type is "top" then
		set sub_categories to get_subcategories(chosen_category)
		log "[debug] recurse to choose_category(sub_categories, \"sub\")"
		set chosen_category to choose_category(sub_categories, "sub")
	end if

	return chosen_category
end choose_category

on get_subcategories(chosen_category)
	set sub_categories to {}
	repeat with this_cat in g_all_categories
		if (this_cat as text) is (chosen_category as text)
			or (this_cat as text) starts with (chosen_category & ":") then
			set end of sub_categories to this_cat as text
		end if
	end repeat
	return sub_categories
end get_subcategories

on set_list_type(cur_type)
	-- verify that 'cur_type' is in 'p_list_types'

	set g_previous_list_type to g_list_type
	set g_list_type to cur_type
end set_list_type

on get_extra_items(list_type)
	if list_type is "top" then

		-- :TODO: Add choice to make a new category

		return {"Show full list with subcategories...", multiplyTxt(uRule, 20)}
	else if list_type is "sub" then
		return {"Show full list of categories...", "Go back to previous list...", multiplyTxt(uRule, 35)}
	else if list_type is "all" then
		return {"Go back to previous list...", multiplyTxt(uRule, 35)}
	else
		return false
	end if
end get_extra_items

on edit_log(log_file, text_editor, should_use_shell)
	(*
		TODO:MAYBE:
		Have a configuration variable in the main script to specify which text
		editor to use. Have another variable for configuring whether or not
		it's a GUI application. Then this function would use 'open' to launch a
		GUI app or script the Terminal to launch a command-line editor.
		Optionally use 'do shell script' to launch a compatible GUI app from a
		command line (so that the working directory can be changed to that of
		the file first), but I don't know if that will really be necessary
		since I have a Vim shortcut for quickly changing the working directory
		if need be.
	*)
	if should_use_shell then
		set s to text_editor & space & log_file & space & "> /dev/null 2>&1"
		do shell script s
		return
	else
		set t to script_name
		set m to ":TODO: edit_log() function: use non-shell-invoked editor."
		set b to {"Cancel"}
		display alert t message m buttons b cancel button 1 as critical
		return false
	end if
end edit_log

-- ===== Utility Functions =====

on convert_to_ascii(non_ascii_txt)
	--
	-- From 'man iconv_open':
	--
	--    When  the string "//TRANSLIT" is appended to tocode, transliteration is
	--    activated. This means that when a character cannot  be  represented  in
	--    the target character set, it can be approximated through one or several
	--    characters that look similar to the original character.
	--
	set s to "iconv -f UTF-8 -t US-ASCII//TRANSLIT <<<" & quoted form of non_ascii_txt
	do shell script s
end convert_to_ascii

on multiply_text(str, n)
	if n < 1 or str = "" then return ""
	set lst to {}
	repeat n times
		set end of lst to str
	end repeat
	return lst as string
end multiply_text

on split_text(txt, delim)
	set old_tids to AppleScript's text item delimiters
	try
		set AppleScript's text item delimiters to (delim as string)
		set lst to every text item of (txt as string)
		set AppleScript's text item delimiters to old_tids
		return lst
	on error err_msg number err_num
		set AppleScript's text item delimiters to old_tids
		error "Can't split_text(): " & err_msg number err_num
	end try
end split_text

on join_list(lst, delim)
	set old_tids to AppleScript's text item delimiters
	try
		set AppleScript's text item delimiters to (delim as string)
		set txt to lst as string
		set AppleScript's text item delimiters to old_tids
		return txt
	on error err_msg number err_num
		set AppleScript's text item delimiters to old_tids
		error "Can't join_list(): " & err_msg number err_num
	end try
end join_list

(************************************************************
 *	Script Objects
 ************************************************************)

script _IO

	(* == PUBLIC == *)

	on write_file(file_path, this_data)
		_write_file(file_path, this_data, string, false) -- overwrite existing file
	end write_file

	on append_file(file_path, this_data)
		_write_file(file_path, this_data, string, true) -- append new data to end of existing file
	end append_file

	on read_file(file_path)
		_read_file(file_path, string)
	end read_file

	(* == PRIVATE == *)

	on _write_file(file_path, this_data, data_class, should_append_data)
		try
			set file_path to file_path as text
			set file_handle to
				open for access file file_path with write permission
			if not should_append_data then
				set eof of the file_handle to 0
			write this_data as data_class to file_handle starting at eof
			close access file_handle
			return true
		on error err_msg number err_num
			try
				close access file file_path
			end try
			error err_msg number err_num
			return false
		end try
	end _write_file

	on _read_file(file_path, data_class)
		try
			set file_path to file_path as text
			if (get eof file file_path) is 0 then
				set file_contents to ""
			else
				set file_contents to read file file_path as data_class
			end if
			return file_contents
		on error
			return 0
		end try
	end _read_file
end script