Script: Refreshable/Portable merged view of files in mixed formats + direct[almost] editing/addition of source files + dynamically linked to the contents of groups/tags

Note - 2020.05.05: Latest version is posted here.

Completion of the previously mentioned proof-of-concept script Proof of concept: Merging a mixture of RTF/RTFD/MD files to MD and with links

What the script does:

  • Merge markdown and RTF/RTFD formatted files into a merged file (MV) that is in markdown format.
  • User can access and edit each original source file directly[almost] within the MV.
  • The MV can be refreshed to reflect the latest change in the source files.
  • If the MV is linked to one/more groups or tags, the MV can be refreshed to include the content of newly added files in those groups/tags.
  • Possible applications: e.g.1 consolidate related notes by tags/groups and maintain dynamic linkage for new content and new files in tags and groups. e.g. 2 simple writings by breaking down the writings into chapters and sections see basic example here.

Demo:

  • Some notes are selected. * I suggest attaching the script to a button.

Note:

  • The selection doesn’t need to be file-only. If a selection includes both files and groups, all of the files in the groups and sub-groups will be merged into the MV.

  • The script asks for options:
    Section 1 is to choose whether to use the selected items or to choose the parent group of the selected files, or the smart groups in the database for the MV.
    Section 2 is to choose the order of sections in the MV. Default means using the order of files that are displayed in DT.
    Section 3 is to ask for adding tags, name, and separator to the end and beginning of each section. If the user needs direct editing of source files within the MV, the option of “Add [[Name]]” must be checked.

Note

  • If the user wants to merge the files but maintaining the hierarchical order of information for items in groups and sub-groups, choose “Default”.
  • If sorted by “Modification/Creation Date” and by “Name” is chosen, all files within the selection (files and files in groups/sub-groups) will be sorted all-together.

  • User may want to link the MV to the parent group of the selection so that the MV will always include the future newly added files.

Note:

  • The parent group of the selection is automatically listed as the first item in the dropdown list, the other groups are all smart groups in the current database.
  • If the selection is the result of the search, there will be no parent group and only the smart groups are listed.

  • The MV is created. Noted that there are both RTF and markdown files in the MV. The formats of the two types of files look similar without needing the user to convert the file format. The arrows point to the name/link of each section’s source notes.

  • Click the link and the MV will switch to each source file (DT’s standard function), and the user can edit the source files directly. The images show that the format of one source file being in markdown and the other in RTF. The user can just click the back button to return to the MV.
    P.S. Can MMD6 do this mixed format linkage? :grinning: :thinking: :thinking:

  • How to refresh the MV to relfect the latest change.
    The MV file must be the frontmost document window and the user just need to click on the script button again.

Note: The user can change the name of the MV after its creation and place it anywhere in DT without affecting the MV’s refresh function.

  • Some hidden metadata is added to the heading of the MV.

  • If the parent group option is checked, or if the selection contains groups, the MV will always include the content of all files (md,rtf,rtfd) in those groups - after refresh.

Note: The groups/tags can have any type of documents (pdf, image, etc) but the MV will only merge the content of RTFD/RTD/markdown files.

The script

To run the script:

  • You need to download “Dialog Toolkit Plus.scptd” and save it under “/Library/Script Libraries” The download link here Dialog Toolkit Plus.
  • A group named “MergeView” must be created at the root of database. All the MVs are saved under "MergedView. But you can move the MV files around after the creation and they will still be refreshable.
use AppleScript version "2.4" -- Yosemite (10.10) or later
use scripting additions
use script "Dialog Toolkit Plus" version "1.1.0"

-- by ngan 2020.04.28
--v1b5 use QsortRecsByName,QsortRecsByMod,QsortRecsByCreate for speed
-- v1b4 add metadata at the heading of MV file
-- v1b3 working version: add selection dialog box and refresable merged view

property MVGpLocation : "/MergeView"
property MVNameFormat : "YMDHNS"
property sortByOptLabel : {"Default", "Modification Date", "Creation Date", "Name"}

-- don't change this
property rtfHeader : "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0 Transitional//EN\" \"http://www.w3.org/TR/REC-html40/loose.dtd\">"


global useSelOrGp, theGps, theGpsUUID, exSubGp, sortBy, sortOrder, addTags, addName, addSeperator
global theRecords, theRecordsCount, theMVContent, theRecordName, eachRecordTags, theSeparator
global newMV, theMV, theMVName, theMVContent, theRTFSource
global rtfCount, mdCount
global gpPopupList, gpPopupLabel

-- Main --
tell application id "DNtp"
	-- check whether the script is to create a new MV or refresh MV
	
	
	if class of think window 1 is viewer window then
		if (count of selection) is not 0 then
			-- call dialog box to get parameters
			set newMV to true
			set theParameters to my getMVOpt()
			if theParameters is "Cancel" then return
		else
			display alert "Select at least one item"
			return
		end if
	else if class of think window 1 is document window then
		set {theMV} to item 1 of {selection}
		set theMVcomment to comment of theMV
		
		if (theMVcomment is "") or (texts 1 thru 2 of theMVcomment is not "MV") then
			display alert "This is not a merged file" giving up after 2
			return
		else
			set newMV to false
			set theParameters to my strToList(theMVcomment, ",")
			-- get praameters from comment of the file
			set useSelOrGp to "S"
			set sortBy to theParameters's item 2
			set sortOrder to theParameters's item 3
			if theParameters's item 4 is "True" then set addTags to true
			if theParameters's item 5 is "True" then set addName to true
			if theParameters's item 6 is "True" then set addSeperator to true
			set theGpsUUID to items 7 thru -1 of theParameters
			set theGps to {}
			repeat with each in theGpsUUID
				set end of theGps to get record with uuid each
			end repeat
		end if
	end if
	
	-- prepare records for merged view	
	set theRecords to my getAllChildren(theGps)
	set {rtfCount, mdCount} to {0, 0}
	set theRecordsCount to length of theRecords
	if sortBy is "N" then -- sort by name option is checked
		my QsortRecsByName(theRecords, 1, theRecordsCount)
		if sortOrder is "D" then set theRecords to reverse of theRecords
		-- set theRecords to my sortRecsByName(theRecords, sortOrder)
	else if sortBy is "M" then -- sort by modification date option is checked
		my QsortRecsByMod(theRecords, 1, theRecordsCount)
		if sortOrder is "D" then set theRecords to reverse of theRecords
		--set theRecords to my sortRecsByMod(theRecords, sortOrder)
	else if sortBy is "C" then -- sort by creation date option is checked
		my QsortRecsByCreate(theRecords, 1, theRecordsCount)
		if sortOrder is "D" then set theRecords to reverse of theRecords
		--set theRecords to my sortRecsByCreate(theRecords, sortOrder)
	end if
	
	
	-- prepare contents
	set theMVContent to ""
	repeat with each in theRecords
		
		-- Set name at section's beginning
		
		if addName then
			if sortBy is "N" or sortBy is "D" then
				set theRecordName to "[[" & each's name & "]]"
			else if sortBy is "M" then
				set theRecordName to "[[" & each's name & "]]" & "    Date Modified: " & my getDateString(each's modification date, "YMD", ".")
			else if sortBy is "C" then
				set theRecordName to "[[" & each's name & "]]" & "    Date Created: " & my getDateString(each's creation date, "YMD", ".")
			end if
		else
			set theRecordName to ""
		end if
		
		-- Set tags at section's ending		
		if addTags then
			--get the tags of each record and convert them into a string of wikilink in [[]] format
			set eachRecordTags to name of (parents of each whose tag type is ordinary tag)
			repeat with i from 1 to length of eachRecordTags
				set eachRecordTags's item i to "[[" & (eachRecordTags's item i) & "]]  "
			end repeat
			if eachRecordTags is not {} then
				set eachRecordTags to my listToStr(my sortlist(eachRecordTags), "  ")
			else
				set eachRecordTags to ""
			end if
		else
			set eachRecordTags to ""
		end if
		
		--Set separator
		if addSeperator then
			set theSeparator to "---"
		else
			set theSeparator to ""
		end if
		
		--prepare the content of theMV
		if type of each is in {rtf, rtfd} then
			set theRTFSource to my findAndReplaceInText(source of each, rtfHeader, "")
			set theMVContent to theMVContent & theRecordName & return & theRTFSource & return & return & eachRecordTags & return & return & theSeparator & return & return
			set rtfCount to rtfCount + 1
		else if type of each is markdown then
			set theMVContent to theMVContent & theRecordName & return & return & plain text of each & return & return & return & eachRecordTags & return & return & theSeparator & return & return & return
			set mdCount to mdCount + 1
		else
			set theRecordsCount to theRecordsCount - 1
		end if
	end repeat
	
	--add metadate to the heading of theMV
	set theMVMetadata to "Last Refreshed: " & (my getDateString(current date, MVNameFormat, ".")) & return & "Total records: " & theRecordsCount & return & "RTF/RTFD files: " & rtfCount & return & "Markdown files: " & mdCount
	set theMVContent to theMVMetadata & return & return & theMVContent
	
	if newMV then
		-- name and create theMV
		set theMVName to "MV - " & (my getDateString(current date, MVNameFormat, "."))
		set theMV to create record with {name:theMVName, source:theMVContent, type:markdown} in (get record at MVGpLocation)
		-- save parameters to comment of MV	
		set comment of theMV to my listToStr({"MV", sortBy, sortOrder, addTags, addName, addSeperator, theGpsUUID}, ",")
		open tab for record theMV
	else
		-- refresh theMV
		set the plain text of theMV to theMVContent
		display alert "The MV file is refreshed, check metadate for info"
	end if
	
end tell
-- end of main program 


--script specific handlers
on getMVOpt()
	
	set {gpPopupList, gpPopupLabel} to my prepareGpsChoice()
	
	-- "Dialog Toolkit Plus" coding
	set accViewWidth to 300
	set theTop to 8
	set {theButtons, minWidth} to create buttons {"Cancel", "OK"} default button 2 given «class btns»:2
	if minWidth > accViewWidth then set accViewWidth to minWidth
	
	set {theRule3, theTop} to create rule (theTop - 8) rule width accViewWidth
	set {addSeperatorOpt, theTop, newWidth} to create checkbox "Add seperator" bottom (theTop + 8) max width accViewWidth / 3 - 8 with initial state
	set {addNameOpt, theTop, newWidth} to create checkbox "Add [[Name]]" bottom (theTop + 8) max width accViewWidth / 3 - 8 with initial state
	set {addTagOpt, theTop, newWidth} to create checkbox "Add [[Tags]]" bottom (theTop + 8) max width accViewWidth / 3 - 8 with initial state
	set {theRule2, theTop} to create rule (theTop + 12) rule width accViewWidth
	set {sortOrderOpt, theTop, newWidth} to create checkbox "Descending (Uncheck = Ascending)" bottom (theTop + 8) max width accViewWidth / 3 - 8 with initial state
	set {sortByOpt, theTop} to create matrix sortByOptLabel bottom (theTop + 8) max width accViewWidth initial choice 1 --without arranged vertically
	set {theRule1, theTop} to create rule (theTop + 12) rule width accViewWidth
	-- set {exSubGpOpt, theTop, newWidth} to create checkbox "Exclude sub-groups" bottom (theTop + 8) max width accViewWidth / 3 - 8 with initial state
	set {gpPopup, theTop} to create popup gpPopupLabel bottom (theTop + 8) popup width accViewWidth initial choice 1
	set {selOpt, theTop} to create matrix {"Use selection", "Use parent group or smart group"} bottom (theTop + 8) max width accViewWidth initial choice 1 --without arranged vertically
	set {theRule0, theTop} to create rule (theTop + 12) rule width accViewWidth
	set {boldLabel, theTop} to create label "For markdown, rtf, rtfd files" bottom theTop + 20 max width accViewWidth control size large size -- aligns center aligned -- with bold type
	-- set allControls to {theRule3, addSeperatorOpt, addNameOpt, addTagOpt, theRule2, sortOrderOpt, sortByOpt, theRule1, exSubGpOpt, gpPopup, selOpt, theRule0, boldLabel}
	set allControls to {theRule3, addSeperatorOpt, addNameOpt, addTagOpt, theRule2, sortOrderOpt, sortByOpt, theRule1, gpPopup, selOpt, theRule0, boldLabel}
	set {buttonName, controlsResults} to display enhanced window "Merge View" acc view width accViewWidth acc view height theTop acc view controls allControls buttons theButtons with align cancel button
	
	
	--  end of "Dialog Toolkit Plus" coding
	if buttonName is not "Cancel" then
		tell application id "DNtp"
			if controlsResults's item 10 is "Use parent group or smart group" then
				-- get the selected gps
				set useSelOrGp to "G"
				set theGps to item (my indexOfOneItem(controlsResults's item 9, gpPopupLabel)) of gpPopupList
			else
				-- get the selection
				set useSelOrGp to "S"
				set theGps to selection as list
				
			end if
			
			set theGpsUUID to {}
			repeat with each in theGps
				set end of theGpsUUID to (get uuid of each)
			end repeat
			
			set sortBy to character 1 of (controlsResults's item 7)
			if controlsResults's item 6 then
				set sortOrder to "D"
			else
				set sortOrder to "A"
			end if
			set addTags to controlsResults's item 4
			set addName to controlsResults's item 3
			set addSeperator to controlsResults's item 2
			
		end tell
		
		return {useSelOrGp, theGps, theGpsUUID, sortBy, sortOrder, addTags, addName, addSeperator}
	else
		return "Cancel"
	end if
end getMVOpt
on prepareGpsChoice()
	local lg, lgn, a, b
	set lgn to {}
	tell application id "DNtp"
		set lg to every smart group of current database
		-- set lg to my sortRecsByName(lg, "A")
		set lgre to length of lg
		my QsortRecsByName(lg, 1, lgre)
		if name of current group is not equal to name of current database then set the beginning of lg to current group
		if lg ≠ {} then
			repeat with each in lg
				set end of lgn to name of each & "      «" & location of each & "»"
			end repeat
			return {lg, lgn}
		else
			return {{}, {"Parent is database and no smart group"}}
		end if
	end tell
end prepareGpsChoice
on getAllChildren(theseGps)
	local l
	set l to {}
	tell application id "DNtp"
		repeat with each in theseGps
			if each's type is group or each's kind is "smart group" then
				set l to l & my getAllChildren(children of each)
			else
				--«constant ctxt»
				if each's type is in {rtfd, rtf, markdown} then set end of l to each's item 1
			end if
		end repeat
	end tell
	return l
end getAllChildren
on QsortRecsByMod(array, leftEnd, rightEnd)
	-- based on Hoare's QuickSort Algorithm
	-- modified by ngan for records sorting in DT
	tell application id "DNtp"
		script a
			property l : array
		end script
		set {i, j} to {leftEnd, rightEnd}
		set v to modification date of item ((leftEnd + rightEnd) div 2) of a's l -- pivot in the middle
		repeat while (j > i)
			repeat while (modification date of (item i of a's l) < v)
				set i to i + 1
			end repeat
			repeat while (modification date of (item j of a's l) > v)
				set j to j - 1
			end repeat
			if (not i > j) then
				tell a's l to set {item i, item j} to {item j, item i} -- swap
				set {i, j} to {i + 1, j - 1}
			end if
		end repeat
		if (leftEnd < j) then my QsortRecsByMod(a's l, leftEnd, j)
		if (rightEnd > i) then my QsortRecsByMod(a's l, i, rightEnd)
	end tell
end QsortRecsByMod
on QsortRecsByName(array, leftEnd, rightEnd)
	-- based on Hoare's QuickSort Algorithm
	-- modified by ngan for records sorting in DT
	tell application id "DNtp"
		script a
			property l : array
		end script
		set {i, j} to {leftEnd, rightEnd}
		set v to name of item ((leftEnd + rightEnd) div 2) of a's l -- pivot in the middle
		repeat while (j > i)
			repeat while (name of (item i of a's l) < v)
				set i to i + 1
			end repeat
			repeat while (name of (item j of a's l) > v)
				set j to j - 1
			end repeat
			if (not i > j) then
				tell a's l to set {item i, item j} to {item j, item i} -- swap
				set {i, j} to {i + 1, j - 1}
			end if
		end repeat
		if (leftEnd < j) then my QsortRecsByName(a's l, leftEnd, j)
		if (rightEnd > i) then my QsortRecsByName(a's l, i, rightEnd)
	end tell
end QsortRecsByName
on QsortRecsByCreate(array, leftEnd, rightEnd)
	-- based on Hoare's QuickSort Algorithm
	-- modified by ngan for records sorting in DT
	tell application id "DNtp"
		script a
			property l : array
		end script
		set {i, j} to {leftEnd, rightEnd}
		set v to creation date of item ((leftEnd + rightEnd) div 2) of a's l -- pivot in the middle
		repeat while (j > i)
			repeat while (creation date of (item i of a's l) < v)
				set i to i + 1
			end repeat
			repeat while (creation date of (item j of a's l) > v)
				set j to j - 1
			end repeat
			if (not i > j) then
				tell a's l to set {item i, item j} to {item j, item i} -- swap
				set {i, j} to {i + 1, j - 1}
			end if
		end repeat
		if (leftEnd < j) then my QsortRecsByCreate(a's l, leftEnd, j)
		if (rightEnd > i) then my QsortRecsByCreate(a's l, i, rightEnd)
	end tell
end QsortRecsByCreate

-- utility handlers
on indexOfOneItem(theItem, theList)
	-- credits Emmanuel Levy
	set {oTIDs, AppleScript's text item delimiters} to {AppleScript's text item delimiters, return}
	set theList to return & theList & return
	set AppleScript's text item delimiters to oTIDs
	try
		
		-1 + (count (paragraphs of (text 1 thru (offset of (return & theItem & return) in theList) of theList)))
	on error
		0
	end try
end indexOfOneItem
on getDateString(theDate, theDateFormat, theSeperator)
	tell application id "DNtp"
		local y, m, d, h, n, s, T
		local lol, ds
		set lol to {{"y", ""}, {"m", ""}, {"d", ""}, {"h", ""}, {"n", ""}, {"s", ""}}
		
		set (lol's item 1)'s item 2 to get year of theDate
		set (lol's item 2)'s item 2 to my padNum((get month of theDate as integer) as string, 2)
		set (lol's item 3)'s item 2 to my padNum((get day of theDate) as string, 2)
		set T to every word of (get time string of theDate)
		set (lol's item 4)'s item 2 to T's item 1
		set (lol's item 5)'s item 2 to T's item 2
		set (lol's item 6)'s item 2 to T's item 3
	end tell
	
	set ds to {}
	set theDateFormat to (every character of theDateFormat)
	repeat with each in theDateFormat
		set ds to ds & (my lolLookup(each as string, 1, 2, lol))'s item 2
	end repeat
	
	return my listToStr(ds, theSeperator)
	
end getDateString
on padNum(lngNum, lngDigits)
	-- Credit houthakker
	set strNum to lngNum as string
	set lngGap to (lngDigits - (length of strNum))
	repeat while lngGap > 0
		set strNum to "0" & strNum
		set lngGap to lngGap - 1
	end repeat
	strNum
end padNum
on lolLookup(lookupVal, lookUpPos, getValPos, theList)
	--only for list of list with more than 1 items
	local i, j, k
	set j to lookUpPos
	set k to getValPos
	repeat with i from 1 to length of theList
		if (item j of item i of theList) is equal to lookupVal then return {i, item k of item i of theList, item i of theList}
	end repeat
	return {0, {}, {}}
end lolLookup
on findAndReplaceInText(theText, theSearchString, theReplacementString)
	set AppleScript's text item delimiters to theSearchString
	set theTextItems to every text item of theText
	set AppleScript's text item delimiters to theReplacementString
	set theText to theTextItems as string
	set AppleScript's text item delimiters to ""
	return theText
end findAndReplaceInText
on listToStr(theList, d)
	local thestr
	set {tid, text item delimiters} to {text item delimiters, d}
	set thestr to theList as text
	set text item delimiters to tid
	return thestr
end listToStr
on strToList(thestr, d)
	local theList
	set {tid, text item delimiters} to {text item delimiters, d}
	set theList to every text item of thestr
	set text item delimiters to tid
	return theList
end strToList
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

6 Likes

A possible usage example for simple writings. (Obviously, I will never switch from Scrivener into this!)

Note

  • In theory, the stack script can be used for version management.
  • There are many[all] writing tools that can do 100x better job that this example!
2 Likes

Another example: I want to quickly review the notes that I had taken by using the qNotes script.

1 Like

A newer version of script that uses a faster sorting algorthrim to handle large number of merged files is updated in the first post.

1 Like

Some features update:

  1. Add internal preference to choose DT-Link or [[…]] link for the name and tags of each section.

Note: Turning on the preference to use DT-Links makes MV file to become portable across different databases. The source files in the MV can be from multiple databases and still be editable directly[almost], and the MV can be refreshed whichever database it is located in DT.

  1. Add option to include pdf files - if there are pdf files in the selection - to display as a link-only section in the MV.

Note: In some situations, the user may want to include the link of pdf files in the MV as reference info.

  1. Add internal preference to show the last refreshed time at the beginning for the MV.

  1. Add progress indicator.

Screenshot 2020-04-30 at 11.35.08

The two new internal preferences

property sectionLinkFormat : "U" -- "W" for [[...]] style link, "U" for DT-URL link 
property showLastRefresh : true -- if true then show the last refeshed time at the header, if false hide the time in metadata

The script:

P.S.

  • I’m writing script at leisure, not to promote a script or workflow.
  • My next attempt is to use the MergeView script to mimic the basic features of Roam Research in create a workflow of either a daily journal, research notes tracking, or a long writings task.
  • Some handlers, such as quick sorting of records, or list-in-list lookup can be quite useful for other members. I usually modified the standard handlers from different sites to be used in DT’s structure (those handlers have credits to the sites or authors).
use AppleScript version "2.4" -- Yosemite (10.10) or later
use scripting additions
use script "Dialog Toolkit Plus" version "1.1.0"

-- by ngan 2020.04.28
-- v1b9 add progress indicator
-- v1b8 add an internal option to use [[...]] or reference url for the name of each file section
-- v1b7 use a different getAllChildren() handler that can include only a specific set of file formats
-- v1b6 add option to include pdf in MV in the form of link
-- v1b5 use QsortRecsByName,QsortRecsByMod,QsortRecsByCreate for speed
-- v1b4 add metadata at the heading of MV file
-- v1b3 working version: add selection dialog box and refresable merged view

property MVGpLocation : "/MergeView"
property MVNameFormat : "YMDHNS" -- yr mon day hr min sec
property sectionLinkFormat : "U" -- "W" for [[...]] style link, "U" for DT-URL link 
property showLastRefresh : true -- if true then show the last refeshed time at the header, if false hide the time in metadata

-- don't change these properties
property rtfHeader : "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0 Transitional//EN\" \"http://www.w3.org/TR/REC-html40/loose.dtd\">"
property sortByOptLabel : {"Default", "Modification Date", "Creation Date", "Name"}

global useSelOrGp, theGps, theGpsUUID, exSubGp, sortBy, sortOrder, addTags, addName, addSeperator, includePDF
global theRecords, theRecordsCount, theMVContent, theRecordName, eachRecordTags, theSeparator
global newMV, theMV, theMVName, theMVContent, theRTFSource
global rtfCount, mdCount, pdfCount
global gpPopupList, gpPopupLabel

-- Main --
tell application id "DNtp"
	-- check whether the script is to create a new MV or refresh MV
	
	
	if class of think window 1 is viewer window then
		show progress indicator "Creating MergeView ..."
		if (count of selection) is not 0 then
			-- call dialog box to get parameters
			set newMV to true
			set theParameters to my getMVOpt()
			if theParameters is "Cancel" then
				hide progress indicator
				return
			end if
		else
			
			display alert "Select at least an item or a group"
			hide progress indicator
			return
		end if
	else if class of think window 1 is document window then
		show progress indicator "Refreshing MergeView ..."
		set {theMV} to item 1 of {selection}
		set theMVcomment to comment of theMV
		
		if (theMVcomment is "") or (texts 1 thru 2 of theMVcomment is not "MV") then
			display alert "This is not a merged file" giving up after 2
			return
		else
			set newMV to false
			set theParameters to my strToList(theMVcomment, ",")
			-- get praameters from comment of the file
			set useSelOrGp to "S"
			set sortBy to theParameters's item 2
			set sortOrder to theParameters's item 3
			if theParameters's item 4 is "True" then
				set addTags to true
			else
				set addTags to false
			end if
			if theParameters's item 5 is "True" then
				set addName to true
			else
				set addName to false
			end if
			if theParameters's item 6 is "True" then
				set addSeperator to true
			else
				set addSeperator to false
			end if
			if theParameters's item 7 is "True" then
				set includePDF to true
			else
				set includePDF to false
			end if
			set theGpsUUID to items 8 thru -1 of theParameters
			set theGps to {}
			repeat with each in theGpsUUID
				set end of theGps to get record with uuid each
			end repeat
		end if
	end if
	
	
	-- prepare records for merged view	for selection w/o pdf
	
	if includePDF then
		set theRecords to my getAllChildren(theGps, {rtfd, rtf, markdown, PDF document})
	else
		set theRecords to my getAllChildren(theGps, {rtfd, rtf, markdown})
	end if
	set {rtfCount, mdCount, pdfCount} to {0, 0, 0}
	set theRecordsCount to length of theRecords
	if sortBy is "N" then -- sort by name option is checked
		my QsortRecsByName(theRecords, 1, theRecordsCount)
		if sortOrder is "D" then set theRecords to reverse of theRecords
		-- set theRecords to my sortRecsByName(theRecords, sortOrder)
	else if sortBy is "M" then -- sort by modification date option is checked
		my QsortRecsByMod(theRecords, 1, theRecordsCount)
		if sortOrder is "D" then set theRecords to reverse of theRecords
		--set theRecords to my sortRecsByMod(theRecords, sortOrder)
	else if sortBy is "C" then -- sort by creation date option is checked
		my QsortRecsByCreate(theRecords, 1, theRecordsCount)
		if sortOrder is "D" then set theRecords to reverse of theRecords
		--set theRecords to my sortRecsByCreate(theRecords, sortOrder)
	end if
	
	
	-- prepare contents
	set theMVContent to ""
	
	set stepCount to 1
	repeat with each in theRecords
		step progress indicator "Merging file ..." & stepCount & "/" & theRecordsCount
		-- Set name at section's beginning
		set theRecordName to ""
		if addName then
			if sectionLinkFormat is "W" then
				-- use [[..]] style wiki link
				if sortBy is "N" or sortBy is "D" then
					set theRecordName to "###### [[" & each's name & "]]"
				else if sortBy is "M" then
					set theRecordName to "###### [[" & each's name & "]]" & "    Modified: " & my getDateString(each's modification date, "YMD", ".")
				else if sortBy is "C" then
					set theRecordName to "###### [[" & each's name & "]]" & "    Created: " & my getDateString(each's creation date, "YMD", ".")
				end if
			else
				-- use reference URL link and set to page 1
				if sortBy is "N" or sortBy is "D" then
					set theRecordName to "###### [" & each's name & "](" & each's reference URL & "?page=0 )"
				else if sortBy is "M" then
					set theRecordName to "###### [" & each's name & "    Modified: " & my getDateString(each's modification date, "YMD", ".") & "](" & each's reference URL & "?page=0 )"
				else if sortBy is "C" then
					set theRecordName to "###### [" & each's name & "    Created: " & my getDateString(each's creation date, "YMD", ".") & "](" & each's reference URL & "?page=o)"
				end if
				
			end if
		end if
		
		-- Set tags at section's ending	
		set eachRecordTags to ""
		
		if addTags then
			if sectionLinkFormat is "W" then
				--get the tags of each record and convert them into a string of wikilink in [[]] format
				set eachRecordTags to name of (parents of each whose tag type is ordinary tag)
				repeat with i from 1 to length of eachRecordTags
					set eachRecordTags's item i to "[[" & (eachRecordTags's item i) & "]]  "
				end repeat
				set eachRecordTags to my listToStr(my sortlist(eachRecordTags), "  ")
				
			else
				-- get reference url and convert them into md link
				set eachRecordTags to (parents of each whose tag type is ordinary tag)
				set ll to {}
				repeat with i from 1 to length of eachRecordTags
					set end of ll to "[" & name of (eachRecordTags's item i) & "](" & reference URL of (eachRecordTags's item i) & ")"
				end repeat
				set eachRecordTags to "**#:** " & my listToStr(my sortlist(ll), "  ")
				
			end if
		end if
		
		--Set separator
		if addSeperator then
			set theSeparator to "---"
		else
			set theSeparator to ""
		end if
		
		--prepare the content of theMV
		if type of each is in {rtf, rtfd} then
			set theRTFSource to my findAndReplaceInText(source of each, rtfHeader, "")
			set theMVContent to theMVContent & theRecordName & return & theRTFSource & return & return & eachRecordTags & return & return & theSeparator & return & return
			set rtfCount to rtfCount + 1
			set stepCount to stepCount + 1
		else if type of each is markdown then
			set theMVContent to theMVContent & theRecordName & return & return & plain text of each & return & return & return & eachRecordTags & return & return & theSeparator & return & return & return
			set mdCount to mdCount + 1
			set stepCount to stepCount + 1
		else if type of each is PDF document and includePDF then
			set pdfLink to ""
			if addName is false then set pdfLink to "[" & each's name & "](" & each's reference URL & ")"
			set theMVContent to theMVContent & theRecordName & return & return & pdfLink & return & return & return & eachRecordTags & return & return & theSeparator & return & return & return
			set pdfCount to pdfCount + 1
			set stepCount to stepCount + 1
		else
			set theRecordsCount to theRecordsCount - 1
			set stepCount to stepCount + 1
		end if
		
	end repeat
	
	--add metadate to the heading of theMV
	if showLastRefresh then
		set theMVMetadata to "Total records: " & theRecordsCount & return & "RTF/RTFD files: " & rtfCount & return & "Markdown files: " & mdCount & return & "PDF files: " & pdfCount & return & return & "Last refreshed: " & (my getDateString(current date, MVNameFormat, ".")) & return & return & theSeparator
	else
		set theMVMetadata to "Last refreshed: " & (my getDateString(current date, MVNameFormat, ".")) & return & "Total records: " & theRecordsCount & return & "RTF/RTFD files: " & rtfCount & return & "Markdown files: " & mdCount & return & "PDF files: " & pdfCount
	end if
	
	set theMVContent to theMVMetadata & return & return & theMVContent
	
	if newMV then
		-- name and create theMV
		set theMVName to "MV - " & (my getDateString(current date, MVNameFormat, "."))
		set theMV to create record with {name:theMVName, source:theMVContent, type:markdown} in (get record at MVGpLocation)
		-- save parameters to comment of MV	
		set comment of theMV to my listToStr({"MV", sortBy, sortOrder, addTags, addName, addSeperator, includePDF, theGpsUUID}, ",")
		open tab for record theMV
		hide progress indicator
	else
		-- refresh theMV
		set the plain text of theMV to theMVContent
		display alert "The MV file is refreshed, check metadate for info" giving up after 1
		hide progress indicator
	end if
	
end tell
-- end of main program 


--script specific handlers
on getMVOpt()
	local addNameOptLabel, addTagOptLabel
	set {gpPopupList, gpPopupLabel} to my prepareGpsChoice()
	if sectionLinkFormat is "W" then
		set addNameOptLabel to "Add [[Name]]"
		set addTagOptLabel to "Add [[Tags]]"
	else
		set addNameOptLabel to "Add Name's Link"
		set addTagOptLabel to "Add Tag's Link"
	end if
	
	
	-- "Dialog Toolkit Plus" coding
	set accViewWidth to 300
	set theTop to 8
	set {theButtons, minWidth} to create buttons {"Cancel", "OK"} default button 2 given «class btns»:2
	if minWidth > accViewWidth then set accViewWidth to minWidth
	
	set {theRule3, theTop} to create rule (theTop - 8) rule width accViewWidth
	set {addSeperatorOpt, theTop, newWidth} to create checkbox "Add seperator" bottom (theTop + 8) max width accViewWidth / 3 - 8 with initial state -- comment out "with initial state" if default==not checked
	
	set {addNameOpt, theTop, newWidth} to create checkbox addNameOptLabel bottom (theTop + 8) max width accViewWidth / 3 - 8 with initial state -- comment out "with initial state" if default==not checked
	set {addTagOpt, theTop, newWidth} to create checkbox addTagOptLabel bottom (theTop + 8) max width accViewWidth / 3 - 8 with initial state -- comment out "with initial state" if default==not checked
	
	set {theRule2, theTop} to create rule (theTop + 12) rule width accViewWidth
	set {sortOrderOpt, theTop, newWidth} to create checkbox "Descending (Uncheck = Ascending)" bottom (theTop + 8) max width accViewWidth / 3 - 8 with initial state -- comment out "with initial state" if default==not checked
	
	set {sortByOpt, theTop} to create matrix sortByOptLabel bottom (theTop + 8) max width accViewWidth initial choice 1
	set {theRule1, theTop} to create rule (theTop + 12) rule width accViewWidth
	set {gpPopup, theTop} to create popup gpPopupLabel bottom (theTop + 8) popup width accViewWidth initial choice 1
	set {selOpt, theTop} to create matrix {"Use selection", "Use parent group or smart group"} bottom (theTop + 8) max width accViewWidth initial choice 1
	set {theRule0, theTop} to create rule (theTop + 12) rule width accViewWidth
	set {addPDFLinkOpt, theTop, newWidth} to create checkbox "Include PDF files as link in MV" bottom (theTop + 8) max width accViewWidth / 3 - 8 --with initial state 	
	set {boldLabel, theTop} to create label "Merge Markdown, RTF, RTFD files" bottom theTop + 8 max width accViewWidth control size large size
	set allControls to {theRule3, addSeperatorOpt, addNameOpt, addTagOpt, theRule2, sortOrderOpt, sortByOpt, theRule1, gpPopup, selOpt, theRule0, addPDFLinkOpt, boldLabel}
	set {buttonName, controlsResults} to display enhanced window "MergeView" acc view width accViewWidth acc view height theTop acc view controls allControls buttons theButtons with align cancel button
	
	
	--  end of "Dialog Toolkit Plus" coding
	if buttonName is not "Cancel" then
		tell application id "DNtp"
			if controlsResults's item 10 is "Use parent group or smart group" then
				-- get the selected gps
				set useSelOrGp to "G"
				set theGps to item (my indexOfOneItem(controlsResults's item 9, gpPopupLabel)) of gpPopupList
			else
				-- get the selection
				set useSelOrGp to "S"
				set theGps to selection as list
				
			end if
			
			set theGpsUUID to {}
			repeat with each in theGps
				set end of theGpsUUID to (get uuid of each)
			end repeat
			
			set sortBy to character 1 of (controlsResults's item 7)
			if controlsResults's item 6 then
				set sortOrder to "D"
			else
				set sortOrder to "A"
			end if
			set addTags to controlsResults's item 4
			set addName to controlsResults's item 3
			set addSeperator to controlsResults's item 2
			set includePDF to controlsResults's item 12
		end tell
		
		return {useSelOrGp, theGps, theGpsUUID, sortBy, sortOrder, addTags, addName, addSeperator, includePDF}
	else
		return "Cancel"
	end if
end getMVOpt
on prepareGpsChoice()
	local lg, lgn, a, b
	set lgn to {}
	tell application id "DNtp"
		set lg to every smart group of current database
		-- set lg to my sortRecsByName(lg, "A")
		set lgre to length of lg
		if lg is not {} then my QsortRecsByName(lg, 1, lgre)
		if name of current group is not equal to name of current database then set the beginning of lg to current group
		if lg ≠ {} then
			repeat with each in lg
				set end of lgn to name of each & "      «" & location of each & "»"
			end repeat
			return {lg, lgn}
		else
			return {{}, {"Parent is database and no smart group"}}
		end if
	end tell
end prepareGpsChoice
on getAllChildren(theseGps, theformat)
	local l
	set l to {}
	tell application id "DNtp"
		repeat with each in theseGps
			if (each's type as string) is in {"group", "smart group"} then
				set l to l & my getAllChildren(children of each, theformat)
			else
				if each's type is in theformat then set end of l to each's item 1
			end if
		end repeat
	end tell
	return l
end getAllChildren
on QsortRecsByMod(array, leftEnd, rightEnd)
	-- based on Hoare's QuickSort Algorithm
	-- modified by ngan for records sorting in DT
	tell application id "DNtp"
		script a
			property l : array
		end script
		set {i, j} to {leftEnd, rightEnd}
		set v to modification date of item ((leftEnd + rightEnd) div 2) of a's l -- pivot in the middle
		repeat while (j > i)
			repeat while (modification date of (item i of a's l) < v)
				set i to i + 1
			end repeat
			repeat while (modification date of (item j of a's l) > v)
				set j to j - 1
			end repeat
			if (not i > j) then
				tell a's l to set {item i, item j} to {item j, item i} -- swap
				set {i, j} to {i + 1, j - 1}
			end if
		end repeat
		if (leftEnd < j) then my QsortRecsByMod(a's l, leftEnd, j)
		if (rightEnd > i) then my QsortRecsByMod(a's l, i, rightEnd)
	end tell
end QsortRecsByMod
on QsortRecsByName(array, leftEnd, rightEnd)
	-- based on Hoare's QuickSort Algorithm
	-- modified by ngan for records sorting in DT
	tell application id "DNtp"
		script a
			property l : array
		end script
		set {i, j} to {leftEnd, rightEnd}
		set v to name of item ((leftEnd + rightEnd) div 2) of a's l -- pivot in the middle
		repeat while (j > i)
			repeat while (name of (item i of a's l) < v)
				set i to i + 1
			end repeat
			repeat while (name of (item j of a's l) > v)
				set j to j - 1
			end repeat
			if (not i > j) then
				tell a's l to set {item i, item j} to {item j, item i} -- swap
				set {i, j} to {i + 1, j - 1}
			end if
		end repeat
		if (leftEnd < j) then my QsortRecsByName(a's l, leftEnd, j)
		if (rightEnd > i) then my QsortRecsByName(a's l, i, rightEnd)
	end tell
end QsortRecsByName
on QsortRecsByCreate(array, leftEnd, rightEnd)
	-- based on Hoare's QuickSort Algorithm
	-- modified by ngan for records sorting in DT
	tell application id "DNtp"
		script a
			property l : array
		end script
		set {i, j} to {leftEnd, rightEnd}
		set v to creation date of item ((leftEnd + rightEnd) div 2) of a's l -- pivot in the middle
		repeat while (j > i)
			repeat while (creation date of (item i of a's l) < v)
				set i to i + 1
			end repeat
			repeat while (creation date of (item j of a's l) > v)
				set j to j - 1
			end repeat
			if (not i > j) then
				tell a's l to set {item i, item j} to {item j, item i} -- swap
				set {i, j} to {i + 1, j - 1}
			end if
		end repeat
		if (leftEnd < j) then my QsortRecsByCreate(a's l, leftEnd, j)
		if (rightEnd > i) then my QsortRecsByCreate(a's l, i, rightEnd)
	end tell
end QsortRecsByCreate

-- utility handlers
on indexOfOneItem(theItem, theList)
	-- credits Emmanuel Levy
	set {oTIDs, AppleScript's text item delimiters} to {AppleScript's text item delimiters, return}
	set theList to return & theList & return
	set AppleScript's text item delimiters to oTIDs
	try
		
		-1 + (count (paragraphs of (text 1 thru (offset of (return & theItem & return) in theList) of theList)))
	on error
		0
	end try
end indexOfOneItem
on getDateString(theDate, theDateFormat, theSeperator)
	tell application id "DNtp"
		local y, m, d, h, n, s, T
		local lol, ds
		set lol to {{"y", ""}, {"m", ""}, {"d", ""}, {"h", ""}, {"n", ""}, {"s", ""}}
		
		set (lol's item 1)'s item 2 to get year of theDate
		set (lol's item 2)'s item 2 to my padNum((get month of theDate as integer) as string, 2)
		set (lol's item 3)'s item 2 to my padNum((get day of theDate) as string, 2)
		set T to every word of (get time string of theDate)
		set (lol's item 4)'s item 2 to T's item 1
		set (lol's item 5)'s item 2 to T's item 2
		set (lol's item 6)'s item 2 to T's item 3
	end tell
	
	set ds to {}
	set theDateFormat to (every character of theDateFormat)
	repeat with each in theDateFormat
		set ds to ds & (my lolLookup(each as string, 1, 2, lol))'s item 2
	end repeat
	
	return my listToStr(ds, theSeperator)
	
end getDateString
on padNum(lngNum, lngDigits)
	-- Credit houthakker
	set strNum to lngNum as string
	set lngGap to (lngDigits - (length of strNum))
	repeat while lngGap > 0
		set strNum to "0" & strNum
		set lngGap to lngGap - 1
	end repeat
	strNum
end padNum
on lolLookup(lookupVal, lookUpPos, getValPos, theList)
	--only for list of list with more than 1 items
	local i, j, k
	set j to lookUpPos
	set k to getValPos
	repeat with i from 1 to length of theList
		if (item j of item i of theList) is equal to lookupVal then return {i, item k of item i of theList, item i of theList}
	end repeat
	return {0, {}, {}}
end lolLookup
on findAndReplaceInText(theText, theSearchString, theReplacementString)
	set AppleScript's text item delimiters to theSearchString
	set theTextItems to every text item of theText
	set AppleScript's text item delimiters to theReplacementString
	set theText to theTextItems as string
	set AppleScript's text item delimiters to ""
	return theText
end findAndReplaceInText
on listToStr(theList, d)
	local thestr
	set {tid, text item delimiters} to {text item delimiters, d}
	set thestr to theList as text
	set text item delimiters to tid
	return thestr
end listToStr
on strToList(thestr, d)
	local theList
	set {tid, text item delimiters} to {text item delimiters, d}
	set theList to every text item of thestr
	set text item delimiters to tid
	return theList
end strToList
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

1 Like

@ngan, this is pretty damn cool!

@Parisko, you might want to take a look at this script.

I am here thinking if there could be a way to randomize footnote numbering in the merge process. I will see if I can find something useful in this direction.

Thanks for your appreciation!

The script is to show that a dynamic and consolidated merged view of multiple items/groups can be achieved within DT. I keep hearing about the interests in seeing multiple files at once but no one is doing it - so I give it a shot. I am now a happy user of the MV script!

I think you can tell from the program that the essence of the script is to put a few sorting and tags/links-to-display parameters, and the uuid of the linked groups/items into the comment of the MV, and then extracting the content and links of each source files into the MV. I see the potential of the script as a core engine for building different workflows/add-ins within DT.

You are very much welcome to modify the script to your specific needs (assuming that the codings are not too clumsy to follow!), particularly that you know the characteristics of markdown and its applications in writings[academic] and other areas much better than me! :grinning: :+1:.

Scalability and performance are my main concerns for this script. The MV refreshing function can handle hundreds of files quite well but I don’t know the limitation of markdown file in DT. I think 100% of markdown files (as the source files in the MV) are easier to handle when it comes to the refresh of the MV - I tested the script with 500+ markdown and it’s doing ok. The second issue is the limitation of sorting performance. The speed of sorting for a large number of records (a few hundred) by AppleScript is less than ideal in DT. You may need to consider these limitations during the development.

Personally, I am exploring how to use the script in the consolidation of academic notes, and perhaps some sort of task/schedule management application (not a keen user of planner, just for fun).

I look forward to seeing you post interesting appliaction in the future!

1 Like

You’re too kind! :slight_smile:

I think I found a way to randomize footnote numbering. It ain’t pretty, but it works.

use AppleScript version "2.4" -- Yosemite (10.10) or later
use scripting additions
use script "RegexAndStuffLib"

set theText to "§19 εἰ δὲ χρὴ καὶ τοῦτο φάναι λόγον ἔχειν, 1103a02 διττὸν ἔσται καὶ τὸ λόγον ἔχον[^2], τὸ μὲν κυρίως καὶ ἐν αὑτῷ, 1103a03 τὸ δ᾽ ὥσπερ τοῦ πατρὸς ἀκουστικόν τι[^1]. 

[^1]: Possível retomada de  [1098a04–5](x-devonthink-item://5F2AE69D-63C3-42CD-AB9F-AC9157A88474?search=1098a04). 
[^2]: Nota 2."


set theString1 to my return_random_string(true, false, false, true, false, 4)
set theText to regex change theText search pattern "\\[\\^1\\]" replace template "[^" & theString1 & "]"

set theString2 to my return_random_string(true, false, false, true, false, 4)
set theText to regex change theText search pattern "\\[\\^2\\]" replace template "[^" & theString2 & "]"

-- And so one for [^3], [^4]...


on return_random_string(include_capitals, include_lowers, include_diacriticals, include_numbers, include_nonalphanumerics, string_length)
	set the_captials to "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
	if include_diacriticals then set the_captials to the_captials & "                    ?"
	set the_lowers to "abcdefghijklmnopqrstuvwxyz"
	if include_diacriticals then set the_lowers to the_lowers & "                    ??"
	set the_numbers to "0123456789"
	set random_pool to {}
	if include_capitals then set end of random_pool to the_captials
	if include_lowers then set end of random_pool to the_lowers
	if include_numbers then set end of random_pool to the_numbers
	if include_nonalphanumerics then
		--this is because the high ASCII chars won't come through in the post:
		repeat with this_num in {33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 58, 59, 60, 61, 62, 63, 64, 91, 92, 93, 94, 95, 96, 123, 124, 125, 126, 127, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 208, 209, 210, 211, 212, 213, 214, 215, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 240, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255}
			set end of random_pool to ASCII character this_num
		end repeat
	end if
	set random_pool to characters of (random_pool as string)
	set return_string to {}
	repeat string_length times
		set end of return_string to (item (random number from 1 to (count random_pool)) of random_pool)
	end repeat
	return return_string as string
end return_random_string

P.s.: I found the handler online :wink:

Here is my butchered version :laughing:
It is a bit slower due to the footnote randomization. All other changes were both minor and idiosyncratic.

use AppleScript version "2.4" -- Yosemite (10.10) or later
use scripting additions
use script "Dialog Toolkit Plus" version "1.1.0"
use script "RegexAndStuffLib"

-- butchered by bcdavasconcelos (2020-05-03-14-06-45)
-- by ngan 2020.04.28
-- v1b9 add progress indicator
-- v1b8 add an internal option to use [[...]] or reference url for the name of each file section
-- v1b7 use a different getAllChildren() handler that can include only a specific set of file formats
-- v1b6 add option to include pdf in MV in the form of link
-- v1b5 use QsortRecsByName,QsortRecsByMod,QsortRecsByCreate for speed
-- v1b4 add metadata at the heading of MV file
-- v1b3 working version: add selection dialog box and refresable merged view

property FNRandomize : true
property MVGpLocation : "/MergeView"
property MVNameFormat : "YMDHNS" -- yr mon day hr min sec
property sectionLinkFormat : "W" -- "W" for [[...]] style link, "U" for DT-URL link 
property showLastRefresh : true -- if true then show the last refeshed time at the header, if false hide the time in metadata


-- don't change these properties
property rtfHeader : "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0 Transitional//EN\" \"http://www.w3.org/TR/REC-html40/loose.dtd\">"
property sortByOptLabel : {"Default", "Modification Date", "Creation Date", "Name"}

global useSelOrGp, theGps, theGpsUUID, exSubGp, sortBy, sortOrder, addTags, addName, addSeparator, includePDF
global theRecords, theRecordsCount, theMVContent, theRecordName, eachRecordTags, theSeparator
global newMV, theMV, theMVName, theMVContent, theRTFSource
global rtfCount, mdCount, pdfCount
global gpPopupList, gpPopupLabel

-- Main --
tell application id "DNtp"
	-- check whether the script is to create a new MV or refresh MV
	
	
	if class of think window 1 is viewer window then
		show progress indicator "Creating MergeView ..."
		if (count of selection) is not 0 then
			-- call dialog box to get parameters
			set newMV to true
			set theParameters to my getMVOpt()
			if theParameters is "Cancel" then
				hide progress indicator
				return
			end if
		else
			
			display alert "Select at least an item or a group"
			hide progress indicator
			return
		end if
	else if class of think window 1 is document window then
		show progress indicator "Refreshing MergeView ..."
		set {theMV} to item 1 of {selection}
		set theMVcomment to comment of theMV
		
		if (theMVcomment is "") or (texts 1 thru 2 of theMVcomment is not "MV") then
			display alert "This is not a merged file" giving up after 2
			return
		else
			set newMV to false
			set theParameters to my strToList(theMVcomment, ",")
			-- get praameters from comment of the file
			set useSelOrGp to "S"
			set sortBy to theParameters's item 2
			set sortOrder to theParameters's item 3
			if theParameters's item 4 is "True" then
				set addTags to true
			else
				set addTags to false
			end if
			if theParameters's item 5 is "True" then
				set addName to true
			else
				set addName to false
			end if
			if theParameters's item 6 is "True" then
				set addSeparator to true
			else
				set addSeparator to false
			end if
			if theParameters's item 7 is "True" then
				set includePDF to true
			else
				set includePDF to false
			end if
			set theGpsUUID to items 8 thru -1 of theParameters
			set theGps to {}
			repeat with each in theGpsUUID
				set end of theGps to get record with uuid each
			end repeat
		end if
	end if
	
	
	-- prepare records for merged view	for selection w/o pdf
	
	if includePDF then
		set theRecords to my getAllChildren(theGps, {rtfd, rtf, markdown, PDF document})
	else
		set theRecords to my getAllChildren(theGps, {rtfd, rtf, markdown})
	end if
	set {rtfCount, mdCount, pdfCount} to {0, 0, 0}
	set theRecordsCount to length of theRecords
	if sortBy is "N" then -- sort by name option is checked
		my QsortRecsByName(theRecords, 1, theRecordsCount)
		if sortOrder is "D" then set theRecords to reverse of theRecords
		-- set theRecords to my sortRecsByName(theRecords, sortOrder)
	else if sortBy is "M" then -- sort by modification date option is checked
		my QsortRecsByMod(theRecords, 1, theRecordsCount)
		if sortOrder is "D" then set theRecords to reverse of theRecords
		--set theRecords to my sortRecsByMod(theRecords, sortOrder)
	else if sortBy is "C" then -- sort by creation date option is checked
		my QsortRecsByCreate(theRecords, 1, theRecordsCount)
		if sortOrder is "D" then set theRecords to reverse of theRecords
		--set theRecords to my sortRecsByCreate(theRecords, sortOrder)
	end if
	
	
	-- prepare contents
	set theMVContent to ""
	
	set stepCount to 1
	repeat with each in theRecords
		step progress indicator "Merging file ..." & stepCount & "/" & theRecordsCount
		-- Set name at section's beginning
		set theRecordName to ""
		if addName then
			if sectionLinkFormat is "W" then
				-- use [[..]] style wiki link
				if sortBy is "N" or sortBy is "D" then
					set theRecordName to "## " & each's name
				else if sortBy is "M" then
					set theRecordName to "## " & each's name & " - Modified: " & my getDateString(each's modification date, "YMD", "-")
				else if sortBy is "C" then
					set theRecordName to "## " & each's name & " - Created: " & my getDateString(each's creation date, "YMD", "-")
				end if
			else
				-- use reference URL link and set to page 1
				if sortBy is "N" or sortBy is "D" then
					set theRecordName to "## [" & each's name & "](" & each's reference URL & "?page=0 )"
				else if sortBy is "M" then
					set theRecordName to "## [" & each's name & "    Modified: " & my getDateString(each's modification date, "YMD", "-") & "](" & each's reference URL & "?page=0 )"
				else if sortBy is "C" then
					set theRecordName to "## [" & each's name & "    Created: " & my getDateString(each's creation date, "YMD", "-") & "](" & each's reference URL & "?page=o)"
				end if
				
			end if
		end if
		
		-- Set tags at section's ending	
		set eachRecordTags to ""
		
		if addTags then
			if sectionLinkFormat is "W" then
				--get the tags of each record and convert them into a string of wikilink in [[]] format
				set eachRecordTags to name of (parents of each whose tag type is ordinary tag)
				repeat with i from 1 to length of eachRecordTags
					set eachRecordTags's item i to (eachRecordTags's item i) & ", "
				end repeat
				set eachRecordTags to my listToStr(my sortlist(eachRecordTags), "  ")
				set eachRecordTags to return & eachRecordTags & return
			else
				-- get reference url and convert them into md link
				set eachRecordTags to (parents of each whose tag type is ordinary tag)
				set ll to {}
				repeat with i from 1 to length of eachRecordTags
					set end of ll to "[" & name of (eachRecordTags's item i) & "](" & reference URL of (eachRecordTags's item i) & ")"
				end repeat
				set eachRecordTags to return & "**#:** " & my listToStr(my sortlist(ll), "  ") & return
				
			end if
		end if
		
		--Set separator
		if addSeparator then
			set theSeparator to return & return & "***"
		else
			set theSeparator to ""
		end if
		
		--prepare the content of theMV
		if type of each is in {rtf, rtfd} then
			set theRTFSource to my findAndReplaceInText(source of each, rtfHeader, "")
			set theMVContent to theMVContent & theRecordName & return & theRTFSource & return & return & eachRecordTags & return & return & theSeparator & return & return
			set rtfCount to rtfCount + 1
			set stepCount to stepCount + 1
		else if type of each is markdown then
			set theText to the plain text of each
			if FNRandomize is true then
				set theString1 to my return_random_string(true, false, false, true, false, 2)
				set theString2 to my return_random_string(true, false, false, true, false, 2)
				set theString3 to my return_random_string(true, false, false, true, false, 2)
				set theString4 to my return_random_string(true, false, false, true, false, 2)
				set theString5 to my return_random_string(true, false, false, true, false, 2)
				set theString6 to my return_random_string(true, false, false, true, false, 2)
				set theString7 to my return_random_string(true, false, false, true, false, 2)
				set theString8 to my return_random_string(true, false, false, true, false, 2)
				set theString9 to my return_random_string(true, false, false, true, false, 2)
				set theString10 to my return_random_string(true, false, false, true, false, 2)
				set theString11 to my return_random_string(true, false, false, true, false, 2)
				set theString12 to my return_random_string(true, false, false, true, false, 2)
				set theString13 to my return_random_string(true, false, false, true, false, 2)
				set theString14 to my return_random_string(true, false, false, true, false, 2)
				set theString15 to my return_random_string(true, false, false, true, false, 2)
				--set theString16 to my return_random_string(true, false, false, true, false, 2)
				--set theString17 to my return_random_string(true, false, false, true, false, 2)
				--set theString18 to my return_random_string(true, false, false, true, false, 2)
				--set theString19 to my return_random_string(true, false, false, true, false, 2)
				--set theString20 to my return_random_string(true, false, false, true, false, 2)
				if theText contains "[^1]" then set theText to regex change theText search pattern "\\[\\^1\\]" replace template "[^" & theString1 & "]"
				if theText contains "[^2]" then set theText to regex change theText search pattern "\\[\\^2\\]" replace template "[^" & theString2 & "]"
				if theText contains "[^3]" then set theText to regex change theText search pattern "\\[\\^3\\]" replace template "[^" & theString3 & "]"
				if theText contains "[^4]" then set theText to regex change theText search pattern "\\[\\^4\\]" replace template "[^" & theString4 & "]"
				if theText contains "[^5]" then set theText to regex change theText search pattern "\\[\\^5\\]" replace template "[^" & theString5 & "]"
				if theText contains "[^6]" then set theText to regex change theText search pattern "\\[\\^6\\]" replace template "[^" & theString6 & "]"
				if theText contains "[^7]" then set theText to regex change theText search pattern "\\[\\^7\\]" replace template "[^" & theString7 & "]"
				if theText contains "[^8]" then set theText to regex change theText search pattern "\\[\\^8\\]" replace template "[^" & theString8 & "]"
				if theText contains "[^9]" then set theText to regex change theText search pattern "\\[\\^9\\]" replace template "[^" & theString9 & "]"
				if theText contains "[^10]" then set theText to regex change theText search pattern "\\[\\^10\\]" replace template "[^" & theString10 & "]"
				if theText contains "[^11]" then set theText to regex change theText search pattern "\\[\\^11\\]" replace template "[^" & theString11 & "]"
				if theText contains "[^12]" then set theText to regex change theText search pattern "\\[\\^12\\]" replace template "[^" & theString12 & "]"
				if theText contains "[^13]" then set theText to regex change theText search pattern "\\[\\^13\\]" replace template "[^" & theString13 & "]"
				if theText contains "[^14]" then set theText to regex change theText search pattern "\\[\\^14\\]" replace template "[^" & theString14 & "]"
				if theText contains "[^15]" then set theText to regex change theText search pattern "\\[\\^15\\]" replace template "[^" & theString15 & "]"
				--set theText to regex change theText search pattern "\\[\\^16\\]" replace template "[^" & theString16 & "]"
				--set theText to regex change theText search pattern "\\[\\^17\\]" replace template "[^" & theString17 & "]"
				--set theText to regex change theText search pattern "\\[\\^18\\]" replace template "[^" & theString18 & "]"
				--set theText to regex change theText search pattern "\\[\\^19\\]" replace template "[^" & theString19 & "]"
				--set theText to regex change theText search pattern "\\[\\^20\\]" replace template "[^" & theString20 & "]"
				
			end if
			set theMVContent to theMVContent & theRecordName & return & theText & eachRecordTags & theSeparator & return
			set mdCount to mdCount + 1
			set stepCount to stepCount + 1
		else if type of each is PDF document and includePDF then
			set pdfLink to ""
			if addName is false then set pdfLink to "[" & each's name & "](" & each's reference URL & ")"
			set theMVContent to theMVContent & theRecordName & return & pdfLink & return & return & return & eachRecordTags & return & return & theSeparator & return & return & return
			set pdfCount to pdfCount + 1
			set stepCount to stepCount + 1
		else
			set theRecordsCount to theRecordsCount - 1
			set stepCount to stepCount + 1
		end if
		
	end repeat
	
	--add metadate to the heading of theMV
	if showLastRefresh then
		set theMVMetadata to "Markdown records: " & mdCount & return & "Last update: " & (my getDateString(current date, MVNameFormat, "-")) & return
		--		set theMVMetadata to "Total records: " & theRecordsCount & return & "RTF/RTFD files: " & rtfCount & return & "Markdown files: " & mdCount & return & "PDF files: " & pdfCount & return & return & "Last refreshed: " & (my getDateString(current date, MVNameFormat, ".")) & return & return & theSeparator
		
	else
		set theMVMetadata to "Last update: " & (my getDateString(current date, MVNameFormat, "-")) & return & "Markdown records: " & mdCount
	end if
	
	-- Remove the last separator before the end of the document
	set theMVContent to theMVMetadata & return & theMVContent
	set theMVContent to regex change theMVContent search pattern return & "\\*\\*\\*" & return & "\\Z" replace template ""
	
	if newMV then
		-- name and create theMV
		set theMVName to "MV - " & (my getDateString(current date, MVNameFormat, "-"))
		set theMV to create record with {name:theMVName, source:theMVContent, type:markdown} in (get record at MVGpLocation)
		-- save parameters to comment of MV	
		set comment of theMV to my listToStr({"MV", sortBy, sortOrder, addTags, addName, addSeparator, includePDF, theGpsUUID}, ",")
		open tab for record theMV
		hide progress indicator
	else
		-- refresh theMV
		set the plain text of theMV to theMVContent
		display alert "The MV file is refreshed, check metadate for info" giving up after 1
		hide progress indicator
	end if
	
end tell
-- end of main program 


--script specific handlers
on getMVOpt()
	local addNameOptLabel, addTagOptLabel
	set {gpPopupList, gpPopupLabel} to my prepareGpsChoice()
	if sectionLinkFormat is "W" then
		set addNameOptLabel to "## Name"
		set addTagOptLabel to "#Tags"
	else
		set addNameOptLabel to "## Name's Link"
		set addTagOptLabel to "#Tag's Link"
	end if
	
	
	-- "Dialog Toolkit Plus" coding
	set accViewWidth to 300
	set theTop to 8
	set {theButtons, minWidth} to create buttons {"Cancel", "OK"} default button 2 given «class btns»:2
	if minWidth > accViewWidth then set accViewWidth to minWidth
	
	set {theRule3, theTop} to create rule (theTop - 8) rule width accViewWidth
	set {addSeparatorOpt, theTop, newWidth} to create checkbox "Add separator" bottom (theTop + 8) max width accViewWidth / 3 - 8 with initial state -- comment out "with initial state" if default==not checked
	
	set {addNameOpt, theTop, newWidth} to create checkbox addNameOptLabel bottom (theTop + 8) max width accViewWidth / 3 - 8 -- comment out "with initial state" if default==not checked
	set {addTagOpt, theTop, newWidth} to create checkbox addTagOptLabel bottom (theTop + 8) max width accViewWidth / 3 - 8 -- comment out "with initial state" if default==not checked
	
	set {theRule2, theTop} to create rule (theTop + 12) rule width accViewWidth
	set {sortOrderOpt, theTop, newWidth} to create checkbox "Descending (Uncheck = Ascending)" bottom (theTop + 8) max width accViewWidth / 3 - 8 -- with initial state -- comment out "with initial state" if default==not checked
	
	set {sortByOpt, theTop} to create matrix sortByOptLabel bottom (theTop + 8) max width accViewWidth initial choice 4
	set {theRule1, theTop} to create rule (theTop + 12) rule width accViewWidth
	set {gpPopup, theTop} to create popup gpPopupLabel bottom (theTop + 8) popup width accViewWidth initial choice 1
	set {selOpt, theTop} to create matrix {"Use selection", "Use parent group or smart group"} bottom (theTop + 8) max width accViewWidth initial choice 1
	set {theRule0, theTop} to create rule (theTop + 12) rule width accViewWidth
	set {addPDFLinkOpt, theTop, newWidth} to create checkbox "Include PDF files as link in MV" bottom (theTop + 8) max width accViewWidth / 3 - 8 --with initial state 	
	set {boldLabel, theTop} to create label "Merge Markdown, RTF, RTFD files" bottom theTop + 8 max width accViewWidth control size large size
	set allControls to {theRule3, addSeparatorOpt, addNameOpt, addTagOpt, theRule2, sortOrderOpt, sortByOpt, theRule1, gpPopup, selOpt, theRule0, addPDFLinkOpt, boldLabel}
	set {buttonName, controlsResults} to display enhanced window "MergeView" acc view width accViewWidth acc view height theTop acc view controls allControls buttons theButtons with align cancel button
	
	
	--  end of "Dialog Toolkit Plus" coding
	if buttonName is not "Cancel" then
		tell application id "DNtp"
			if controlsResults's item 10 is "Use parent group or smart group" then
				-- get the selected gps
				set useSelOrGp to "G"
				set theGps to item (my indexOfOneItem(controlsResults's item 9, gpPopupLabel)) of gpPopupList
			else
				-- get the selection
				set useSelOrGp to "S"
				set theGps to selection as list
				
			end if
			
			set theGpsUUID to {}
			repeat with each in theGps
				set end of theGpsUUID to (get uuid of each)
			end repeat
			
			set sortBy to character 1 of (controlsResults's item 7)
			if controlsResults's item 6 then
				set sortOrder to "D"
			else
				set sortOrder to "A"
			end if
			set addTags to controlsResults's item 4
			set addName to controlsResults's item 3
			set addSeparator to controlsResults's item 2
			set includePDF to controlsResults's item 12
		end tell
		
		return {useSelOrGp, theGps, theGpsUUID, sortBy, sortOrder, addTags, addName, addSeparator, includePDF}
	else
		return "Cancel"
	end if
end getMVOpt
on prepareGpsChoice()
	local lg, lgn, a, b
	set lgn to {}
	tell application id "DNtp"
		set lg to every smart group of current database
		-- set lg to my sortRecsByName(lg, "A")
		set lgre to length of lg
		if lg is not {} then my QsortRecsByName(lg, 1, lgre)
		if name of current group is not equal to name of current database then set the beginning of lg to current group
		if lg ≠ {} then
			repeat with each in lg
				set end of lgn to name of each & "      «" & location of each & "»"
			end repeat
			return {lg, lgn}
		else
			return {{}, {"Parent is database and no smart group"}}
		end if
	end tell
end prepareGpsChoice
on getAllChildren(theseGps, theformat)
	local l
	set l to {}
	tell application id "DNtp"
		repeat with each in theseGps
			if (each's type as string) is in {"group", "smart group"} then
				set l to l & my getAllChildren(children of each, theformat)
			else
				if each's type is in theformat then set end of l to each's item 1
			end if
		end repeat
	end tell
	return l
end getAllChildren
on QsortRecsByMod(array, leftEnd, rightEnd)
	-- based on Hoare's QuickSort Algorithm
	-- modified by ngan for records sorting in DT
	tell application id "DNtp"
		script a
			property l : array
		end script
		set {i, j} to {leftEnd, rightEnd}
		set v to modification date of item ((leftEnd + rightEnd) div 2) of a's l -- pivot in the middle
		repeat while (j > i)
			repeat while (modification date of (item i of a's l) < v)
				set i to i + 1
			end repeat
			repeat while (modification date of (item j of a's l) > v)
				set j to j - 1
			end repeat
			if (not i > j) then
				tell a's l to set {item i, item j} to {item j, item i} -- swap
				set {i, j} to {i + 1, j - 1}
			end if
		end repeat
		if (leftEnd < j) then my QsortRecsByMod(a's l, leftEnd, j)
		if (rightEnd > i) then my QsortRecsByMod(a's l, i, rightEnd)
	end tell
end QsortRecsByMod
on QsortRecsByName(array, leftEnd, rightEnd)
	-- based on Hoare's QuickSort Algorithm
	-- modified by ngan for records sorting in DT
	tell application id "DNtp"
		script a
			property l : array
		end script
		set {i, j} to {leftEnd, rightEnd}
		set v to name of item ((leftEnd + rightEnd) div 2) of a's l -- pivot in the middle
		repeat while (j > i)
			repeat while (name of (item i of a's l) < v)
				set i to i + 1
			end repeat
			repeat while (name of (item j of a's l) > v)
				set j to j - 1
			end repeat
			if (not i > j) then
				tell a's l to set {item i, item j} to {item j, item i} -- swap
				set {i, j} to {i + 1, j - 1}
			end if
		end repeat
		if (leftEnd < j) then my QsortRecsByName(a's l, leftEnd, j)
		if (rightEnd > i) then my QsortRecsByName(a's l, i, rightEnd)
	end tell
end QsortRecsByName
on QsortRecsByCreate(array, leftEnd, rightEnd)
	-- based on Hoare's QuickSort Algorithm
	-- modified by ngan for records sorting in DT
	tell application id "DNtp"
		script a
			property l : array
		end script
		set {i, j} to {leftEnd, rightEnd}
		set v to creation date of item ((leftEnd + rightEnd) div 2) of a's l -- pivot in the middle
		repeat while (j > i)
			repeat while (creation date of (item i of a's l) < v)
				set i to i + 1
			end repeat
			repeat while (creation date of (item j of a's l) > v)
				set j to j - 1
			end repeat
			if (not i > j) then
				tell a's l to set {item i, item j} to {item j, item i} -- swap
				set {i, j} to {i + 1, j - 1}
			end if
		end repeat
		if (leftEnd < j) then my QsortRecsByCreate(a's l, leftEnd, j)
		if (rightEnd > i) then my QsortRecsByCreate(a's l, i, rightEnd)
	end tell
end QsortRecsByCreate

-- utility handlers
on indexOfOneItem(theItem, theList)
	-- credits Emmanuel Levy
	set {oTIDs, AppleScript's text item delimiters} to {AppleScript's text item delimiters, return}
	set theList to return & theList & return
	set AppleScript's text item delimiters to oTIDs
	try
		
		-1 + (count (paragraphs of (text 1 thru (offset of (return & theItem & return) in theList) of theList)))
	on error
		0
	end try
end indexOfOneItem
on getDateString(theDate, theDateFormat, theSeparator)
	tell application id "DNtp"
		local y, m, d, h, n, s, T
		local lol, ds
		set lol to {{"y", ""}, {"m", ""}, {"d", ""}, {"h", ""}, {"n", ""}, {"s", ""}}
		
		set (lol's item 1)'s item 2 to get year of theDate
		set (lol's item 2)'s item 2 to my padNum((get month of theDate as integer) as string, 2)
		set (lol's item 3)'s item 2 to my padNum((get day of theDate) as string, 2)
		set T to every word of (get time string of theDate)
		set (lol's item 4)'s item 2 to T's item 1
		set (lol's item 5)'s item 2 to T's item 2
		set (lol's item 6)'s item 2 to T's item 3
	end tell
	
	set ds to {}
	set theDateFormat to (every character of theDateFormat)
	repeat with each in theDateFormat
		set ds to ds & (my lolLookup(each as string, 1, 2, lol))'s item 2
	end repeat
	
	return my listToStr(ds, theSeparator)
	
end getDateString
on padNum(lngNum, lngDigits)
	-- Credit houthakker
	set strNum to lngNum as string
	set lngGap to (lngDigits - (length of strNum))
	repeat while lngGap > 0
		set strNum to "0" & strNum
		set lngGap to lngGap - 1
	end repeat
	strNum
end padNum
on lolLookup(lookupVal, lookUpPos, getValPos, theList)
	--only for list of list with more than 1 items
	local i, j, k
	set j to lookUpPos
	set k to getValPos
	repeat with i from 1 to length of theList
		if (item j of item i of theList) is equal to lookupVal then return {i, item k of item i of theList, item i of theList}
	end repeat
	return {0, {}, {}}
end lolLookup
on findAndReplaceInText(theText, theSearchString, theReplacementString)
	set AppleScript's text item delimiters to theSearchString
	set theTextItems to every text item of theText
	set AppleScript's text item delimiters to theReplacementString
	set theText to theTextItems as string
	set AppleScript's text item delimiters to ""
	return theText
end findAndReplaceInText
on listToStr(theList, d)
	local thestr
	set {tid, text item delimiters} to {text item delimiters, d}
	set thestr to theList as text
	set text item delimiters to tid
	return thestr
end listToStr
on strToList(thestr, d)
	local theList
	set {tid, text item delimiters} to {text item delimiters, d}
	set theList to every text item of thestr
	set text item delimiters to tid
	return theList
end strToList
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 return_random_string(include_capitals, include_lowers, include_diacriticals, include_numbers, include_nonalphanumerics, string_length)
	set the_captials to "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
	if include_diacriticals then set the_captials to the_captials & "                    ?"
	set the_lowers to "abcdefghijklmnopqrstuvwxyz"
	if include_diacriticals then set the_lowers to the_lowers & "                    ??"
	set the_numbers to "0123456789"
	set random_pool to {}
	if include_capitals then set end of random_pool to the_captials
	if include_lowers then set end of random_pool to the_lowers
	if include_numbers then set end of random_pool to the_numbers
	if include_nonalphanumerics then
		--this is because the high ASCII chars won't come through in the post:
		repeat with this_num in {33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 58, 59, 60, 61, 62, 63, 64, 91, 92, 93, 94, 95, 96, 123, 124, 125, 126, 127, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 208, 209, 210, 211, 212, 213, 214, 215, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 240, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255}
			set end of random_pool to ASCII character this_num
		end repeat
	end if
	set random_pool to characters of (random_pool as string)
	set return_string to {}
	repeat string_length times
		set end of return_string to (item (random number from 1 to (count random_pool)) of random_pool)
	end repeat
	return return_string as string
end return_random_string

1 Like

Great, I will check it out next week!

This is brilliant! Thank you so much @ngan !

I’m a big fan of working with very granular modules that go into different combinations of groups and smart groups, but this makes it challenging to have contextual continuity when reading and editing. I’ve been really wanting a Scrivener-like option to “see the forest AND the trees” within DT!

Yet another super-useful script I get to install from you :sunny:

2 Likes

I incorporate the elements of my qNote script and add one feature.

The feature: allowing the user to create new files (Markdown or RTF) through the MV into the any group/sub-group that are the sources of the MV. This feature can be a time saver because user no longer need to find the right group in the DT’s main window, add the file/note they need, and go back to refresh the MV.

Demo:

  • Assuming that a user wants to add a new section 2.4 in the MV.

  • The script will ask for action when it is called to refresh the MV.

Note: This feature can be disabled by setting the internal preference - not everyone needs it.

  • The user can choose from a list of groups/sub-groups that are consolidated into the MV. The file will be created in the selected group.

Note:

  1. The 1st line in the text box will be used as the filename.
  2. User cannot add files if no group was selected in the MV during its creation.
  3. User cannot add files if tags/smart group are the source info of the MV (shouldn’t create items under tags directly and there is no easy way to identify the parent group for items in a smart group). But files can be created if group tags are the source.
  4. User can set a basic reminder (i.e. the same function as qNote).

  • The MV will be refreshed after a file is added.

How to turn-off add file feature

Change the value of askAddFileWhenRefresh from true to false

property MVGpLocation : "/MergeView"
property MVNameFormat : "YMDHNS" -- yr mon day hr min sec
property sectionLinkFormat : "U" -- "W" for [[...]] style link, "U" for DT-URL link 
property showLastRefresh : true -- if true then show the last refresh time at the header, if false hide the time in metadata
property askAddFileWhenRefresh : true

The script:

Note: There are just a few new lines and two handlers:

-- the new lines of coding
if askAddFileWhenRefresh then
	set theAns to button returned of (display alert "Choose Action" & return & "MV will be refreshed after the file is added." message "" buttons {"Add File", "Refresh"} default button 2)
	if theAns is "Add File" then my addNotes()
	activate
end if

-- the 2 newly added handlers:
-- on addNotes()
-- on getAllGps(theseGps, theTab)
use AppleScript version "2.4" -- Yosemite (10.10) or later
use scripting additions
use script "Dialog Toolkit Plus" version "1.1.0"

-- by ngan 2020.04.28
-- v1b10 add new RTF/Markdown file to group within MV window
-- v1b9 add progress indicator
-- v1b8 add an internal option to use [[...]] or reference url for the name of each file section
-- v1b7 use a different getAllChildren() handler that can include only a specific set of file formats
-- v1b6 add option to include pdf in MV in the form of link
-- v1b5 use QsortRecsByName,QsortRecsByMod,QsortRecsByCreate for speed
-- v1b4 add metadata at the heading of MV file
-- v1b3 working version: add selection dialog box and refresable merged view

property MVGpLocation : "/MergeView"
property MVNameFormat : "YMDHNS" -- yr mon day hr min sec
property sectionLinkFormat : "U" -- "W" for [[...]] style link, "U" for DT-URL link 
property showLastRefresh : true -- if true then show the last refeshed time at the header, if false hide the time in metadata
property askAddFileWhenRefresh : true

-- don't change these properties
property rtfHeader : "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0 Transitional//EN\" \"http://www.w3.org/TR/REC-html40/loose.dtd\">"
property sortByOptLabel : {"Default", "Modification Date", "Creation Date", "Name"}
property reminderTypeList : {"No Reminder", "Alert", "Notification", "Speak Text", "Send mail", "Launch URL"}
property alarmDateTimeFormat : "dmy" -- macOS system date format for UK, day followed by month by year
--end of don't change these properties



global useSelOrGp, theGps, theGpsUUID, exSubGp, sortBy, sortOrder, addTags, addName, addSeperator, includePDF
global theRecords, theRecordsCount, theMVContent, theRecordName, eachRecordTags, theSeparator
global newMV, theMV, theMVName, theMVContent, theRTFSource
global rtfCount, mdCount, pdfCount
global gpPopupList, gpPopupLabel


-- Main --
tell application id "DNtp"
	-- check whether the script is to create a new MV or refresh MV
	
	
	if class of think window 1 is viewer window then
		show progress indicator "Creating MergeView ..."
		if (count of selection) is not 0 then
			-- call dialog box to get parameters
			set newMV to true
			set theParameters to my getMVOpt()
			if theParameters is "Cancel" then
				hide progress indicator
				return
			end if
		else
			
			display alert "Select at least an item or a group"
			hide progress indicator
			return
		end if
	else if class of think window 1 is document window then
		show progress indicator "Refreshing MergeView ..."
		set {theMV} to item 1 of {selection}
		set theMVcomment to comment of theMV
		
		if (theMVcomment is "") or (texts 1 thru 2 of theMVcomment is not "MV") then
			display alert "This is not a merged file" giving up after 2
			return
		else
			set newMV to false
			set theParameters to my strToList(theMVcomment, ",")
			-- get praameters from comment of the file
			set useSelOrGp to "S"
			set sortBy to theParameters's item 2
			set sortOrder to theParameters's item 3
			if theParameters's item 4 is "True" then
				set addTags to true
			else
				set addTags to false
			end if
			if theParameters's item 5 is "True" then
				set addName to true
			else
				set addName to false
			end if
			if theParameters's item 6 is "True" then
				set addSeperator to true
			else
				set addSeperator to false
			end if
			if theParameters's item 7 is "True" then
				set includePDF to true
			else
				set includePDF to false
			end if
			set theGpsUUID to items 8 thru -1 of theParameters
			set theGps to {}
			repeat with each in theGpsUUID
				set end of theGps to get record with uuid each
			end repeat
			
			if askAddFileWhenRefresh then
				set theAns to button returned of (display alert "Choose Action" & return & "MV will be refreshed after the file is added." message "" buttons {"Add File", "Refresh"} default button 2)
				if theAns is "Add File" then my addNotes()
				activate
			end if
			
		end if
	end if
	
	
	-- prepare records for merged view	for selection w/o pdf
	
	if includePDF then
		set theRecords to my getAllChildren(theGps, {rtfd, rtf, markdown, PDF document})
	else
		set theRecords to my getAllChildren(theGps, {rtfd, rtf, markdown})
	end if
	set {rtfCount, mdCount, pdfCount} to {0, 0, 0}
	set theRecordsCount to length of theRecords
	if sortBy is "N" then -- sort by name option is checked
		my QsortRecsByName(theRecords, 1, theRecordsCount)
		if sortOrder is "D" then set theRecords to reverse of theRecords
		-- set theRecords to my sortRecsByName(theRecords, sortOrder)
	else if sortBy is "M" then -- sort by modification date option is checked
		my QsortRecsByMod(theRecords, 1, theRecordsCount)
		if sortOrder is "D" then set theRecords to reverse of theRecords
		--set theRecords to my sortRecsByMod(theRecords, sortOrder)
	else if sortBy is "C" then -- sort by creation date option is checked
		my QsortRecsByCreate(theRecords, 1, theRecordsCount)
		if sortOrder is "D" then set theRecords to reverse of theRecords
		--set theRecords to my sortRecsByCreate(theRecords, sortOrder)
	end if
	
	
	-- prepare contents
	set theMVContent to ""
	
	set stepCount to 1
	repeat with each in theRecords
		step progress indicator "Merging file ..." & stepCount & "/" & theRecordsCount
		-- Set name at section's beginning
		set theRecordName to ""
		if addName then
			if sectionLinkFormat is "W" then
				-- use [[..]] style wiki link
				if sortBy is "N" or sortBy is "D" then
					set theRecordName to "###### [[" & each's name & "]]"
				else if sortBy is "M" then
					set theRecordName to "###### [[" & each's name & "]]" & "    Modified: " & my getDateString(each's modification date, "YMD", ".")
				else if sortBy is "C" then
					set theRecordName to "###### [[" & each's name & "]]" & "    Created: " & my getDateString(each's creation date, "YMD", ".")
				end if
			else
				-- use reference URL link and set to page 1
				if sortBy is "N" or sortBy is "D" then
					set theRecordName to "###### [" & each's name & "](" & each's reference URL & "?page=0 )"
				else if sortBy is "M" then
					set theRecordName to "###### [" & each's name & "    Modified: " & my getDateString(each's modification date, "YMD", ".") & "](" & each's reference URL & "?page=0 )"
				else if sortBy is "C" then
					set theRecordName to "###### [" & each's name & "    Created: " & my getDateString(each's creation date, "YMD", ".") & "](" & each's reference URL & "?page=o)"
				end if
				
			end if
		end if
		
		-- Set tags at section's ending	
		set eachRecordTags to ""
		
		if addTags then
			if sectionLinkFormat is "W" then
				--get the tags of each record and convert them into a string of wikilink in [[]] format
				set eachRecordTags to name of (parents of each whose tag type is ordinary tag)
				repeat with i from 1 to length of eachRecordTags
					set eachRecordTags's item i to "[[" & (eachRecordTags's item i) & "]]  "
				end repeat
				set eachRecordTags to my listToStr(my sortlist(eachRecordTags), "  ")
				
			else
				-- get reference url and convert them into md link
				set eachRecordTags to (parents of each whose tag type is ordinary tag)
				set ll to {}
				repeat with i from 1 to length of eachRecordTags
					set end of ll to "[" & name of (eachRecordTags's item i) & "](" & reference URL of (eachRecordTags's item i) & ")"
				end repeat
				set eachRecordTags to "**#:** " & my listToStr(my sortlist(ll), "  ")
				
			end if
		end if
		
		--Set separator
		if addSeperator then
			set theSeparator to "---"
		else
			set theSeparator to ""
		end if
		
		--prepare the content of theMV
		if type of each is in {rtf, rtfd} then
			set theRTFSource to my findAndReplaceInText(source of each, rtfHeader, "")
			set theMVContent to theMVContent & theRecordName & return & theRTFSource & return & return & eachRecordTags & return & return & theSeparator & return & return
			set rtfCount to rtfCount + 1
			set stepCount to stepCount + 1
		else if type of each is markdown then
			set theMVContent to theMVContent & theRecordName & return & return & plain text of each & return & return & return & eachRecordTags & return & return & theSeparator & return & return & return
			set mdCount to mdCount + 1
			set stepCount to stepCount + 1
		else if type of each is PDF document and includePDF then
			set pdfLink to ""
			if addName is false then set pdfLink to "[" & each's name & "](" & each's reference URL & ")"
			set theMVContent to theMVContent & theRecordName & return & return & pdfLink & return & return & return & eachRecordTags & return & return & theSeparator & return & return & return
			set pdfCount to pdfCount + 1
			set stepCount to stepCount + 1
		else
			set theRecordsCount to theRecordsCount - 1
			set stepCount to stepCount + 1
		end if
		
	end repeat
	
	--add metadate to the heading of theMV
	if showLastRefresh then
		set theMVMetadata to "Total records: " & theRecordsCount & return & "RTF/RTFD files: " & rtfCount & return & "Markdown files: " & mdCount & return & "PDF files: " & pdfCount & return & return & "Last refreshed: " & (my getDateString(current date, MVNameFormat, ".")) & return & return & theSeparator
	else
		set theMVMetadata to "Last refreshed: " & (my getDateString(current date, MVNameFormat, ".")) & return & "Total records: " & theRecordsCount & return & "RTF/RTFD files: " & rtfCount & return & "Markdown files: " & mdCount & return & "PDF files: " & pdfCount
	end if
	
	set theMVContent to theMVMetadata & return & return & theMVContent
	
	if newMV then
		-- name and create theMV
		set theMVName to "MV - " & (my getDateString(current date, MVNameFormat, "."))
		set theMV to create record with {name:theMVName, source:theMVContent, type:markdown} in (get record at MVGpLocation)
		-- save parameters to comment of MV	
		set comment of theMV to my listToStr({"MV", sortBy, sortOrder, addTags, addName, addSeperator, includePDF, theGpsUUID}, ",")
		open tab for record theMV
		hide progress indicator
	else
		-- refresh theMV
		set the plain text of theMV to theMVContent
		display alert "The MV file is refreshed, check metadate for info" giving up after 1
		hide progress indicator
	end if
	
end tell
-- end of main program 


--script specific handlers
on getMVOpt()
	local addNameOptLabel, addTagOptLabel
	set {gpPopupList, gpPopupLabel} to my prepareGpsChoice()
	if sectionLinkFormat is "W" then
		set addNameOptLabel to "Add [[Name]]"
		set addTagOptLabel to "Add [[Tags]]"
	else
		set addNameOptLabel to "Add Name's Link"
		set addTagOptLabel to "Add Tag's Link"
	end if
	
	
	-- "Dialog Toolkit Plus" coding
	set accViewWidth to 300
	set theTop to 8
	set {theButtons, minWidth} to create buttons {"Cancel", "OK"} default button 2 given «class btns»:2
	if minWidth > accViewWidth then set accViewWidth to minWidth
	
	set {theRule3, theTop} to create rule (theTop - 8) rule width accViewWidth
	set {addSeperatorOpt, theTop, newWidth} to create checkbox "Add seperator" bottom (theTop + 8) max width accViewWidth / 3 - 8 with initial state -- comment out "with initial state" if default==not checked
	
	set {addNameOpt, theTop, newWidth} to create checkbox addNameOptLabel bottom (theTop + 8) max width accViewWidth / 3 - 8 with initial state -- comment out "with initial state" if default==not checked
	set {addTagOpt, theTop, newWidth} to create checkbox addTagOptLabel bottom (theTop + 8) max width accViewWidth / 3 - 8 with initial state -- comment out "with initial state" if default==not checked
	
	set {theRule2, theTop} to create rule (theTop + 12) rule width accViewWidth
	set {sortOrderOpt, theTop, newWidth} to create checkbox "Descending (Uncheck = Ascending)" bottom (theTop + 8) max width accViewWidth / 3 - 8 with initial state -- comment out "with initial state" if default==not checked
	
	set {sortByOpt, theTop} to create matrix sortByOptLabel bottom (theTop + 8) max width accViewWidth initial choice 1
	set {theRule1, theTop} to create rule (theTop + 12) rule width accViewWidth
	set {gpPopup, theTop} to create popup gpPopupLabel bottom (theTop + 8) popup width accViewWidth initial choice 1
	set {selOpt, theTop} to create matrix {"Use selection", "Use parent group or smart group"} bottom (theTop + 8) max width accViewWidth initial choice 1
	set {theRule0, theTop} to create rule (theTop + 12) rule width accViewWidth
	set {addPDFLinkOpt, theTop, newWidth} to create checkbox "Include PDF files as link in MV" bottom (theTop + 8) max width accViewWidth / 3 - 8 --with initial state 	
	set {boldLabel, theTop} to create label "Merge Markdown, RTF, RTFD files" bottom theTop + 8 max width accViewWidth control size large size
	set allControls to {theRule3, addSeperatorOpt, addNameOpt, addTagOpt, theRule2, sortOrderOpt, sortByOpt, theRule1, gpPopup, selOpt, theRule0, addPDFLinkOpt, boldLabel}
	set {buttonName, controlsResults} to display enhanced window "MergeView" acc view width accViewWidth acc view height theTop acc view controls allControls buttons theButtons with align cancel button
	
	
	--  end of "Dialog Toolkit Plus" coding
	if buttonName is not "Cancel" then
		tell application id "DNtp"
			if controlsResults's item 10 is "Use parent group or smart group" then
				-- get the selected gps
				set useSelOrGp to "G"
				set theGps to item (my indexOfOneItem(controlsResults's item 9, gpPopupLabel)) of gpPopupList
			else
				-- get the selection
				set useSelOrGp to "S"
				set theGps to selection as list
				
			end if
			
			set theGpsUUID to {}
			repeat with each in theGps
				set end of theGpsUUID to (get uuid of each)
			end repeat
			
			set sortBy to character 1 of (controlsResults's item 7)
			if controlsResults's item 6 then
				set sortOrder to "D"
			else
				set sortOrder to "A"
			end if
			set addTags to controlsResults's item 4
			set addName to controlsResults's item 3
			set addSeperator to controlsResults's item 2
			set includePDF to controlsResults's item 12
		end tell
		
		return {useSelOrGp, theGps, theGpsUUID, sortBy, sortOrder, addTags, addName, addSeperator, includePDF}
	else
		return "Cancel"
	end if
end getMVOpt
on prepareGpsChoice()
	local lg, lgn, tgu
	set lgn to {}
	tell application id "DNtp"
		set lg to every smart group of current database
		-- set lg to my sortRecsByName(lg, "A")
		set lgre to length of lg
		if lg is not {} then my QsortRecsByName(lg, 1, lgre)
		if name of current group is not equal to name of current database then set the beginning of lg to current group
		
		if lg ≠ {} then
			repeat with each in lg
				set end of lgn to name of each & "      «" & location of each & "»"
			end repeat
			return {lg, lgn}
		else
			return {{}, {"Parent is database and no smart group"}, tgu}
		end if
	end tell
end prepareGpsChoice
on addNotes()
	-- this handler uses global variables in the main program for input
	set theSpacer to "	" -- spacer used to display the group hierarchy in the dialog box
	
	tell application id "DNtp"
		set lg to my getAllGps(theGps, theSpacer) -- lg holds all items in MV that are group or tag group
		if lg is {} then
			display alert "There is no gorup in the MV. Refreshing MV now." giving up after 1
			return
		end if
		set ln to {}
		set idx to 1
		repeat with each in lg
			set end of ln to (idx & "." & each's item 2 as string)
			set idx to idx + 1
		end repeat
		
	end tell
	
	set qNoteGpsName to ln
	
	-- "Dialog Toolkit Plus" coding
	set accViewWidth to 600
	set {theButtons, minWidth} to create buttons {"Cancel", "OK"} default button 2 given «class btns»:2
	if minWidth > accViewWidth then set accViewWidth to minWidth -- make sure buttons fit
	set {noteField, noteLabel, theTop} to create top labeled field "" placeholder text "Type filename at the first line." bottom 0 field width accViewWidth extra height 400 label text "Add file:" with accepts linebreak and tab
	set {theRule2, theTop} to create rule (theTop + 12) rule width accViewWidth
	
	set {alarmTextField, alarmTextLabel, theTop, fieldLeft} to create side labeled field "" placeholder text "Type message/email address/url here" bottom (theTop + 8) total width accViewWidth label text "Reminder message:    " field left 0
	
	set {addReminderPopup, theTop} to create popup reminderTypeList left inset 0 bottom (theTop + 8) popup width 160 initial choice 1
	
	set {alarmDateTimeField, theTop} to create field (my getDateString(current date, alarmDateTimeFormat, ".") & " " & my getDateString(current date, "hn", ":")) left inset 170 bottom (theTop - 22) field width accViewWidth - 170
	
	set {theRule1, theTop} to create rule (theTop + 12) rule width accViewWidth
	
	set {openNotePopup, openNoteLabel, theTop} to create labeled matrix {"Save Only", "Save+Open"} bottom (theTop + 8) max width accViewWidth - 300 matrix left 0 label text "Action: " initial choice 1 without arranged vertically
	
	set {fileFormatPopup, fileFormatLabel, theTop} to create labeled matrix {"Markdown", "Rich Text"} bottom (theTop + 8) max width accViewWidth - 300 matrix left 0 label text "Format:" initial choice 1 without arranged vertically
	
	set {locPopup, locLabel, theTop} to create labeled popup qNoteGpsName bottom (theTop + 8) popup width 250 max width accViewWidth label text "Where:  " popup left 0 initial choice 1
	
	set allControls to {noteField, noteLabel, theRule2, alarmTextField, alarmTextLabel, addReminderPopup, alarmDateTimeField, theRule1, openNotePopup, openNoteLabel, fileFormatPopup, fileFormatLabel, locPopup, locLabel}
	
	set {buttonName, controlsResults} to display enhanced window "MergeView - Add Note" acc view width accViewWidth acc view height theTop acc view controls allControls buttons theButtons active field noteField with align cancel button
	--  end of "Dialog Toolkit Plus" coding
	
	-- credit @Bluefrog's snippet
	--get parameters of reminder
	set reminderDateTime to (date (controlsResults's item 7 as string))
	set addReminder to controlsResults's item 6
	set reminderMessage to controlsResults's item 4
	
	tell application id "DNtp"
		if buttonName is "OK" then
			set theDate to my getDateString(current date, "ymdhn", ".")
			
			
			if controlsResults's item 1 is not "" then
				-- use 1st line of note as filename	
				set theName to first paragraph of controlsResults's item 1
			else
				-- if user has forgotton to add file name
				set theName to "Created by MV on " & theDate
			end if
			
			--get content of the note
			set theContent to controlsResults's item 1
			-- get the action for save only or save + open
			set openNoteAfterCreation to controlsResults's item 9
			--get the format of the note to be saved
			set fileFormat to controlsResults's item 11
			
			-- get the location to save the note
			-- k is a temporary variable to get the position of group in theGps
			set k to texts 1 thru ((offset of "." in controlsResults's item 13) - 1) of controlsResults's item 13
			set qNoteGp to (item k of lg)'s item 1
			
			
			if fileFormat is "Markdown" then
				
				--check for no entry
				if controlsResults's item 1 is not "" then
					
					set theContent to every paragraph of theContent
					-- bold first line (the filename)
					-- DO not include the filename in the content
					set theContent to rest of theContent
					-- there are two hiddem return return in the line-belowed
					set theContent to my listToStr(theContent, "
	")
				else
					set theContent to ""
				end if
				set theNote to create record with {name:theName, source:theContent, type:markdown} in qNoteGp
			else if fileFormat is "Rich Text" then
				set theNote to create record with {name:theName, source:theContent, type:rtfd} in qNoteGp
			end if
			log message "A file named " & theName & " is created in " & location of qNoteGp
			set unread of theNote to true
			
			-- set reminder
			if addReminder is "Alert" then
				tell theNote to make new reminder with properties {schedule:once, alarm:alert, alarm string:reminderMessage, due date:reminderDateTime}
			else if addReminder is "Notification" then
				tell theNote to make new reminder with properties {schedule:once, alarm:notification, alarm string:reminderMessage, due date:reminderDateTime}
			else if addReminder is "Speak Text" then
				tell theNote to make new reminder with properties {schedule:once, alarm:speak, alarm string:reminderMessage, due date:reminderDateTime}
			else if addReminder is "Send mail" then
				tell theNote to make new reminder with properties {schedule:once, alarm:mail, alarm string:reminderMessage, due date:reminderDateTime}
			else if addReminder is "Launch URL" then
				set the URL of theNote to reminderMessage
				tell theNote to make new reminder with properties {schedule:once, alarm:launch, due date:reminderDateTime}
			end if
			
			if openNoteAfterCreation is "Save+Open" then
				open tab for record theNote
			end if
		end if
		
	end tell
	
	
	
end addNotes
on getAllGps(theseGps, theTab)
	local l
	set l to {}
	tell application id "DNtp"
		set idx to 0
		repeat with each in theseGps
			
			if each's type is group and each's tag type is not ordinary tag then
				set idx to idx + 1
				set end of l to {each's item 1, theTab & ((each's item 1)'s name)}
				set l to l & my getAllGps(children of each, theTab & theTab)
				
			end if
		end repeat
	end tell
	return l
end getAllGps
on getAllChildren(theseGps, theformat)
	local l
	set l to {}
	tell application id "DNtp"
		repeat with each in theseGps
			if (each's type as string) is in {"group", "smart group"} then
				set l to l & my getAllChildren(children of each, theformat)
			else
				if each's type is in theformat then set end of l to each's item 1
			end if
		end repeat
	end tell
	return l
end getAllChildren
on QsortRecsByMod(array, leftEnd, rightEnd)
	-- based on Hoare's QuickSort Algorithm
	-- modified by ngan for records sorting in DT
	tell application id "DNtp"
		script a
			property l : array
		end script
		set {i, j} to {leftEnd, rightEnd}
		set v to modification date of item ((leftEnd + rightEnd) div 2) of a's l -- pivot in the middle
		repeat while (j > i)
			repeat while (modification date of (item i of a's l) < v)
				set i to i + 1
			end repeat
			repeat while (modification date of (item j of a's l) > v)
				set j to j - 1
			end repeat
			if (not i > j) then
				tell a's l to set {item i, item j} to {item j, item i} -- swap
				set {i, j} to {i + 1, j - 1}
			end if
		end repeat
		if (leftEnd < j) then my QsortRecsByMod(a's l, leftEnd, j)
		if (rightEnd > i) then my QsortRecsByMod(a's l, i, rightEnd)
	end tell
end QsortRecsByMod
on QsortRecsByName(array, leftEnd, rightEnd)
	-- based on Hoare's QuickSort Algorithm
	-- modified by ngan for records sorting in DT
	tell application id "DNtp"
		script a
			property l : array
		end script
		set {i, j} to {leftEnd, rightEnd}
		set v to name of item ((leftEnd + rightEnd) div 2) of a's l -- pivot in the middle
		repeat while (j > i)
			repeat while (name of (item i of a's l) < v)
				set i to i + 1
			end repeat
			repeat while (name of (item j of a's l) > v)
				set j to j - 1
			end repeat
			if (not i > j) then
				tell a's l to set {item i, item j} to {item j, item i} -- swap
				set {i, j} to {i + 1, j - 1}
			end if
		end repeat
		if (leftEnd < j) then my QsortRecsByName(a's l, leftEnd, j)
		if (rightEnd > i) then my QsortRecsByName(a's l, i, rightEnd)
	end tell
end QsortRecsByName
on QsortRecsByCreate(array, leftEnd, rightEnd)
	-- based on Hoare's QuickSort Algorithm
	-- modified by ngan for records sorting in DT
	tell application id "DNtp"
		script a
			property l : array
		end script
		set {i, j} to {leftEnd, rightEnd}
		set v to creation date of item ((leftEnd + rightEnd) div 2) of a's l -- pivot in the middle
		repeat while (j > i)
			repeat while (creation date of (item i of a's l) < v)
				set i to i + 1
			end repeat
			repeat while (creation date of (item j of a's l) > v)
				set j to j - 1
			end repeat
			if (not i > j) then
				tell a's l to set {item i, item j} to {item j, item i} -- swap
				set {i, j} to {i + 1, j - 1}
			end if
		end repeat
		if (leftEnd < j) then my QsortRecsByCreate(a's l, leftEnd, j)
		if (rightEnd > i) then my QsortRecsByCreate(a's l, i, rightEnd)
	end tell
end QsortRecsByCreate

-- utility handlers
on indexOfOneItem(theItem, theList)
	-- credits Emmanuel Levy
	set {oTIDs, AppleScript's text item delimiters} to {AppleScript's text item delimiters, return}
	set theList to return & theList & return
	set AppleScript's text item delimiters to oTIDs
	try
		
		-1 + (count (paragraphs of (text 1 thru (offset of (return & theItem & return) in theList) of theList)))
	on error
		0
	end try
end indexOfOneItem
on getDateString(theDate, theDateFormat, theSeperator)
	tell application id "DNtp"
		local y, m, d, h, n, s, T
		local lol, ds
		set lol to {{"y", ""}, {"m", ""}, {"d", ""}, {"h", ""}, {"n", ""}, {"s", ""}}
		
		set (lol's item 1)'s item 2 to get year of theDate
		set (lol's item 2)'s item 2 to my padNum((get month of theDate as integer) as string, 2)
		set (lol's item 3)'s item 2 to my padNum((get day of theDate) as string, 2)
		set T to every word of (get time string of theDate)
		set (lol's item 4)'s item 2 to T's item 1
		set (lol's item 5)'s item 2 to T's item 2
		set (lol's item 6)'s item 2 to T's item 3
	end tell
	
	set ds to {}
	set theDateFormat to (every character of theDateFormat)
	repeat with each in theDateFormat
		set ds to ds & (my lolLookup(each as string, 1, 2, lol))'s item 2
	end repeat
	
	return my listToStr(ds, theSeperator)
	
end getDateString
on padNum(lngNum, lngDigits)
	-- Credit houthakker
	set strNum to lngNum as string
	set lngGap to (lngDigits - (length of strNum))
	repeat while lngGap > 0
		set strNum to "0" & strNum
		set lngGap to lngGap - 1
	end repeat
	strNum
end padNum
on lolLookup(lookupVal, lookUpPos, getValPos, theList)
	--only for list of list with more than 1 items
	local i, j, k
	set j to lookUpPos
	set k to getValPos
	repeat with i from 1 to length of theList
		if (item j of item i of theList) is equal to lookupVal then return {i, item k of item i of theList, item i of theList}
	end repeat
	return {0, {}, {}}
end lolLookup
on findAndReplaceInText(theText, theSearchString, theReplacementString)
	set AppleScript's text item delimiters to theSearchString
	set theTextItems to every text item of theText
	set AppleScript's text item delimiters to theReplacementString
	set theText to theTextItems as string
	set AppleScript's text item delimiters to ""
	return theText
end findAndReplaceInText
on listToStr(theList, d)
	local thestr
	set {tid, text item delimiters} to {text item delimiters, d}
	set thestr to theList as text
	set text item delimiters to tid
	return thestr
end listToStr
on strToList(thestr, d)
	local theList
	set {tid, text item delimiters} to {text item delimiters, d}
	set theList to every text item of thestr
	set text item delimiters to tid
	return theList
end strToList
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


1 Like

That’s a very handy addition @ngan! Thank you!

Is “using parent group / smart group” merged view working for you after v1b5? Up to that version it was working fine for me, but from v1b9 that you posted later, when I run the script I get a blank page. The same happens if I select “use selection” for a selection of groups. Not a problem for a selection of files with the “use selection” option.

On another note, do you think it would be possible to somehow get the group names displayed at their relevant locations in the merged view? This might get in the way for some workflows, but if it could be set as an optional preference this could really help making sense of the structure of the selection.

Lastly, have you tried complementing your workflow with OPML exports? I’ve found DT’s OPML export to be very handy. I have a keyboard shortcut assigned to it, and opening it (or dragging and dropping) in mind mappers and outliners helps visualising things rather quickly (in Tinderbox is great because other metadata and a link to DT is added). The contents of text files (md,rtf,txt) are parsed as titles, so, if you follow the convention of having the first line of the text as its title, you don’t lose any info. (Comments are parsed as notes, and they work for groups as well as documents). Not as nice as having it straight into DT like with your script, and not so easily updatable, but if you need to hide/show, expand/collapse a selection it may complement the workflow.

Thanks for letting me know. Can you kindly try to run the script in external script editor instead of putting it in DT’s script menu or toolbar and see what’s happening?

P.S. If you are running the script from a button, please try this:

  • First, delete the script from the toolbar folder in macOS
    ( ~/library/Application Scripts/com.devon-technologies.think3/Toolbar ).
  • Second, place the script under the DT’s script menu folder instead. Assigning a shortcut keys combination is more convenient.
  • Restart DT3.

Running the script in the DT’s Script Menu by following the above steps will probably solve the problem in the short-run. I encountered the same issue just very recently when running the script from the toolbar folder as a button but no issue when running the script from the DT’s or Apple’s Script menu. Still trying to find the real reason but the script is doing what it’s supposed to do.

Thanks for the suggestion. I’ll work on this - it’s a bit more tricky but should be doable.

No idea on OPML. I am just an amateur and have always been working within DT’s environment only… :sweat_smile:

This works, thank you! However, if I run it from DT it still doesn’t work for groups, eventhough I’m running the script from the menu, and not from a button. The progress bar displays a message “Creating Mergeview…” and nothing happens. But don’t worry about it, I can launch the script from the script editor and assign a keybord shortcut for that too.

Thank you! Only if it also makes sense to you!

If you’re an amateur and you write those scripts I guess that leaves me as an aspiring amateur! :sweat_smile:

If you want to give OPML a try, you can install Simplemind or Freemind for example, select a group you want to visualise its parent-child structures, go to File - Export - As Opml… and drag and drop that file into the mind mapper. It also works with Scrivener’s Import - File, but all the text documents’ content are shown as titles.

Thanks to @Eds for the suggestion.

The changes in v1b13:

  • The option of “Add Tag’s Link” is now changed to “Add Tag’s and Parent’s Link”.
  • Bug fix. The script can now run as a button script again. Previous v1b10/11 can’t.
  • The clean-up of html codes in RTF files in MV is more thorough now. The look-and-feel of files in different formats should be more consistent in the MV.

Demo:

Note

  • The option of showing the tag’s and parent’s link can’t be separated because the performance of the script deteriorates for each additional option.
  • Getting the direct parent group of an item at the time of selection is too tricky (for now) when the item has replicants. The script can only display all parents (group and group tag) of an item. (EDITED 20.05.07: decided not to show the direct parent group - the feature works but slow down the script a lot.)
  • I have added quite a few error-checking statements in the script. If anyone sees one during the testing, please let me know. Thanks!
  • The issue with v1b10/11 is not related with 3rd part additions. It’s just a minor issue relating to how DT deal with type of files in AppleScript.

The Script

A very minor bug fix is posted on 2020.05.08. If the option “Add Tag’s and Parent’s Link” is unchecked, you might see an error message in the previous download of v1b13. This version v1b13.1 should fix the issue.

use AppleScript version "2.4" -- Yosemite (10.10) or later
use scripting additions
use script "Dialog Toolkit Plus" version "1.1.0"

-- by ngan 2020.04.28
--v1b13.1 Bug fix. the script should run as a button script now
-- v1b11 (1) add display parents of each item in MV (2) add try statement for debugging
-- v1b10 add insert file to group within MV window
-- v1b9 add progress indicator
-- v1b8 add an internal option to use [[...]] or reference url for the name of each file section
-- v1b7 use a different getAllChildren() handler that can include only a specific set of file formats
-- v1b6 add option to include pdf in MV in the form of link
-- v1b5 use QsortRecsByName,QsortRecsByMod,QsortRecsByCreate for speed
-- v1b4 add metadata at the heading of MV file
-- v1b3 working version: add selection dialog box and refresable merged view

property MVGpLocation : "/MergeView"
property MVNameFormat : "YMDHNS" -- yr mon day hr min sec
property sectionLinkFormat : "U" -- "W" for [[...]] style link, "U" for DT-URL link 
property showLastRefresh : true -- if true then show the last refeshed time at the header, if alse hide the time in metadata
property askAddFileWhenRefresh : true

-- don't change these properties
--property rtfHeader : "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.0 Transitional//EN\" \"http://www.w3.org/TR/REC-html40/loose.dtd\">"
property sortByOptLabel : {"Default", "Modification Date", "Creation Date", "Name"}
property reminderTypeList : {"No Reminder", "Alert", "Notification", "Speak Text", "Send mail", "Launch URL"}
property alarmDateTimeFormat : "dmy" -- macOS system date format for UK, day followed by month by year
--end of don't change these properties

global useSelOrGp, theGps, theGpsUUID, sortBy, sortOrder, addTags, addName, addSeperator, includePDF
global theRecords, gpList, gpListLabel

-- Main --
tell application id "DNtp"
	
	
	
	if class of think window 1 is viewer window then
		try
			if (count of selection) is 0 then
				display alert "Select at least an item or a group"
				return
				
			else
				show progress indicator "Creating MergeView ..."
				
				set newMV to true
				-- gpList, gpListLabel are used by getMVOpt()
				set {gpList, gpListLabel} to my prepareGpsChoice()
				-- call the dialog box and return all options
				set theParameters to my getMVOpt()
				if theParameters is "Cancel" then
					hide progress indicator
					return
				else
					-- convert options into internal variables
					if theParameters's item 10 is "Use parent group or smart group" then
						-- get the selected gps
						set useSelOrGp to "G"
						set theGps to item (my indexOfOneItem(theParameters's item 9, gpListLabel)) of gpList
					else if theParameters's item 10 is "Use selection" then
						-- get the selection
						set useSelOrGp to "S"
						set theGps to selection as list
					end if
					
					set theGpsUUID to {}
					repeat with each in theGps
						set end of theGpsUUID to (get uuid of each's item 1)
					end repeat
					
					set sortBy to character 1 of (theParameters's item 7)
					if theParameters's item 6 then
						set sortOrder to "D"
					else
						set sortOrder to "A"
					end if
					set addTags to theParameters's item 4
					set addName to theParameters's item 3
					set addSeperator to theParameters's item 2
					set includePDF to theParameters's item 12
				end if
				
			end if
		on error
			display dialog "Err Main 1"
		end try
		
	else if class of think window 1 is document window then
		
		show progress indicator "Refreshing MergeView ..."
		set {theMV} to item 1 of {selection}
		set theMVcomment to comment of theMV
		
		if (theMVcomment is "") or (texts 1 thru 2 of theMVcomment is not "MV") then
			display alert "This is not a MergeView file" giving up after 2
			return
		else
			set newMV to false
			set theParameters to my strToList(theMVcomment, ",")
			-- get praameters from comment of the file
			set useSelOrGp to "S"
			set sortBy to theParameters's item 2
			set sortOrder to theParameters's item 3
			if theParameters's item 4 is "True" then
				set addTags to true
			else
				set addTags to false
			end if
			if theParameters's item 5 is "True" then
				set addName to true
			else
				set addName to false
			end if
			if theParameters's item 6 is "True" then
				set addSeperator to true
			else
				set addSeperator to false
			end if
			if theParameters's item 7 is "True" then
				set includePDF to true
			else
				set includePDF to false
			end if
			
			set theGpsUUID to items 8 thru -1 of theParameters
			
			set theGps to {}
			repeat with each in theGpsUUID
				set end of theGps to get record with uuid each
			end repeat
			
			-- Ask add file or refresh the MV
			if askAddFileWhenRefresh then
				try
					set theAns to button returned of (display alert "Choose Action" & return & "MV will be refreshed after the file is added." message "" buttons {"Add File", "Refresh"} default button 2)
					if theAns is "Add File" then my addNotes()
				on error
					display dialog "Err Main 2"
				end try
			end if
			
		end if
	end if
	
	
	-- prepare records for merged view	for selection w/o pdf
	
	if includePDF then
		set theRecords to my getAllChildren(theGps, {rtf, rtfd, markdown, PDF document})
	else
		set theRecords to my getAllChildren(theGps, {rtf, rtfd, markdown})
	end if
	
	--Error checking
	if theRecords is {} then
		display alert "Err Main 3"
		return
	end if
	
	-- counter for meta data at MV's header
	set {rtfCount, mdCount, pdfCount} to {0, 0, 0}
	set theRecordsCount to length of theRecords
	
	-- Sort option
	if sortBy is "N" then -- sort by name option is checked
		my QsortRecsByName(theRecords, 1, theRecordsCount)
		if sortOrder is "D" then set theRecords to reverse of theRecords
		-- set theRecords to my sortRecsByName(theRecords, sortOrder)
	else if sortBy is "M" then -- sort by modification date option is checked
		my QsortRecsByMod(theRecords, 1, theRecordsCount)
		if sortOrder is "D" then set theRecords to reverse of theRecords
		--set theRecords to my sortRecsByMod(theRecords, sortOrder)
	else if sortBy is "C" then -- sort by creation date option is checked
		my QsortRecsByCreate(theRecords, 1, theRecordsCount)
		if sortOrder is "D" then set theRecords to reverse of theRecords
		--set theRecords to my sortRecsByCreate(theRecords, sortOrder)
	end if
	
	-- prepare contents
	set theMVContent to ""
	set stepCount to 1 -- for progress indicator
	repeat with each in theRecords
		step progress indicator "Merging file ..." & stepCount & "/" & theRecordsCount
		-- Set each section's name
		set theRecordName to ""
		if addName then
			try
				if sectionLinkFormat is "W" then
					-- use [[..]] style wiki link
					if sortBy is "N" or sortBy is "D" then
						set theRecordName to "###### [[" & each's name & "]]"
					else if sortBy is "M" then
						set theRecordName to "###### [[" & each's name & "]]" & "    Modified: " & my getDateString(each's modification date, "YMD", ".")
					else if sortBy is "C" then
						set theRecordName to "###### [[" & each's name & "]]" & "    Created: " & my getDateString(each's creation date, "YMD", ".")
					end if
				else if sectionLinkFormat is "U" then
					-- use reference URL link and set to page 1
					if sortBy is "N" or sortBy is "D" then
						set theRecordName to "###### [" & each's name & "](" & each's reference URL & "?page=0 )"
					else if sortBy is "M" then
						set theRecordName to "###### [" & each's name & "    Modified: " & my getDateString(each's modification date, "YMD", ".") & "](" & each's reference URL & "?page=0 )"
					else if sortBy is "C" then
						set theRecordName to "###### [" & each's name & "    Created: " & my getDateString(each's creation date, "YMD", ".") & "](" & each's reference URL & "?page=o)"
					end if
				end if
			on error
				display dialog "Err Main 4"
			end try
		end if
		
		
		set {eachRecordTags, eachRecordParents} to {"", ""}
		-- Set each section's tags and parents name
		if addTags then
			try
				
				set {tagLabel, parentLabel} to {"", ""}
				
				if sectionLinkFormat is "W" then
					--get the tags of each record and convert them into a string of wikilink in [[]] format
					set eachRecordTags to name of (parents of each whose tag type is ordinary tag)
					repeat with i from 1 to length of eachRecordTags
						set eachRecordTags's item i to "[[" & (eachRecordTags's item i) & "]]  "
					end repeat
					if eachRecordTags is not {} then set tagLabel to "**T:** "
					set eachRecordTags to tagLabel & my listToStr(my sortlist(eachRecordTags), "  ")
					
					set eachRecordParents to name of (parents of each whose tag type is not ordinary tag)
					repeat with i from 1 to length of eachRecordParents
						set eachRecordParents's item i to "[[" & (eachRecordParents's item i) & "]]  "
					end repeat
					if eachRecordParents is not {} then set parentLabel to "**G:** "
					set eachRecordParents to parentLabel & my listToStr(my sortlist(eachRecordParents), "  ")
					
					
				else
					-- get reference url and convert them into md link
					set eachRecordTags to (parents of each whose tag type is ordinary tag)
					set ll to {}
					repeat with i from 1 to length of eachRecordTags
						set end of ll to "[" & name of (eachRecordTags's item i) & "](" & reference URL of (eachRecordTags's item i) & ")"
					end repeat
					if ll is not {} then set tagLabel to "**T:** "
					set eachRecordTags to tagLabel & my listToStr(my sortlist(ll), "  ")
					
					
					set eachRecordParents to (parents of each whose tag type is not ordinary tag)
					set ll to {}
					repeat with i from 1 to length of eachRecordParents
						set end of ll to "[" & name of (eachRecordParents's item i) & "](" & reference URL of (eachRecordParents's item i) & ")"
					end repeat
					if ll is not {} then set parentLabel to "**G:** "
					set eachRecordParents to parentLabel & my listToStr(my sortlist(ll), "  ")
					
				end if
				
			on error
				display dialog "Err Main 5"
			end try
		end if
		
		
		--Set separator
		if addSeperator then
			set theSeparator to "---"
		else
			set theSeparator to ""
		end if
		
		try
			--prepare the content of theMV
			if type of each is in {rtf, rtfd} then
				-- set theRTFSource to my findAndReplaceInText(source of each, rtfHeader, "")
				set theRTFSource to my getRTFContent(source of each)
				set theMVContent to theMVContent & theRecordName & return & theRTFSource & return & return & eachRecordTags & return & return & eachRecordParents & return & return & theSeparator & return & return
				set rtfCount to rtfCount + 1
				set stepCount to stepCount + 1
			else if type of each is markdown then
				set theMVContent to theMVContent & theRecordName & return & return & plain text of each & return & return & return & eachRecordTags & return & return & eachRecordParents & return & return & theSeparator & return & return & return
				set mdCount to mdCount + 1
				set stepCount to stepCount + 1
			else if type of each is PDF document and includePDF then
				set pdfLink to ""
				if addName is false then set pdfLink to "[" & each's name & "](" & each's reference URL & ")"
				set theMVContent to theMVContent & theRecordName & return & return & pdfLink & return & return & return & eachRecordTags & return & return & eachRecordParents & return & return & theSeparator & return & return & return
				set pdfCount to pdfCount + 1
				set stepCount to stepCount + 1
			else
				set theRecordsCount to theRecordsCount - 1
				set stepCount to stepCount + 1
			end if
		on error
			display dialog "Err Main 6"
		end try
	end repeat
	
	try
		--add metadate to theMV's header
		if showLastRefresh then
			set theMVMetadata to "Total records: " & theRecordsCount & return & "RTF/RTFD files: " & rtfCount & return & "Markdown files: " & mdCount & return & "PDF files: " & pdfCount & return & return & "Last refreshed: " & (my getDateString(current date, MVNameFormat, ".")) & return & return & theSeparator
		else
			set theMVMetadata to "Last refreshed: " & (my getDateString(current date, MVNameFormat, ".")) & return & "Total records: " & theRecordsCount & return & "RTF/RTFD files: " & rtfCount & return & "Markdown files: " & mdCount & return & "PDF files: " & pdfCount
		end if
		
		set theMVContent to theMVMetadata & return & return & theMVContent
		
		
	on error
		display dialog "Err Main 7"
	end try
	
	
	try
		if newMV then
			-- name and create theMV
			set theMVName to "MV - " & (my getDateString(current date, MVNameFormat, "."))
			set theMV to create record with {name:theMVName, source:theMVContent, type:markdown} in (get record at MVGpLocation)
			-- save parameters to comment of MV	
			set comment of theMV to my listToStr({"MV", sortBy, sortOrder, addTags, addName, addSeperator, includePDF, theGpsUUID}, ",")
			open tab for record theMV
			hide progress indicator
		else
			-- refresh theMV
			set the plain text of theMV to theMVContent
			display alert "The MV file is refreshed, check metadate for info" giving up after 1
			hide progress indicator
		end if
		
	on error
		display dialog "Err Main 8"
	end try
end tell
-- end of main program 


--script specific handlers
on getMVOpt()
	try
		if sectionLinkFormat is "W" then
			set addNameOptLabel to "Add [[Name]]"
			set addTagOptLabel to "Add [[Tags]] & [[Parents]]"
		else
			set addNameOptLabel to "Add Name's Link"
			set addTagOptLabel to "Add Tag's and Parent's Link"
		end if
		
		
		-- "Dialog Toolkit Plus" coding
		set accViewWidth to 300
		set theTop to 8
		set {theButtons, minWidth} to create buttons {"Cancel", "OK"} default button 2 given «class btns»:2
		if minWidth > accViewWidth then set accViewWidth to minWidth
		
		set {theRule3, theTop} to create rule (theTop - 8) rule width accViewWidth
		set {addSeperatorOpt, theTop, newWidth} to create checkbox "Add seperator" bottom (theTop + 8) max width accViewWidth / 3 - 8 with initial state -- comment out "with initial state" if default==not checked
		
		set {addNameOpt, theTop, newWidth} to create checkbox addNameOptLabel bottom (theTop + 8) max width accViewWidth / 3 - 8 with initial state -- comment out "with initial state" if default==not checked
		set {addTagOpt, theTop, newWidth} to create checkbox addTagOptLabel bottom (theTop + 8) max width accViewWidth / 3 - 8 with initial state -- comment out "with initial state" if default==not checked
		
		set {theRule2, theTop} to create rule (theTop + 12) rule width accViewWidth
		set {sortOrderOpt, theTop, newWidth} to create checkbox "Descending (Uncheck = Ascending)" bottom (theTop + 8) max width accViewWidth / 3 - 8 with initial state -- comment out "with initial state" if default==not checked
		
		set {sortByOpt, theTop} to create matrix sortByOptLabel bottom (theTop + 8) max width accViewWidth initial choice 1
		set {theRule1, theTop} to create rule (theTop + 12) rule width accViewWidth
		set {gpPopup, theTop} to create popup gpListLabel bottom (theTop + 8) popup width accViewWidth initial choice 1
		set {selOpt, theTop} to create matrix {"Use selection", "Use parent group or smart group"} bottom (theTop + 8) max width accViewWidth initial choice 1
		set {theRule0, theTop} to create rule (theTop + 12) rule width accViewWidth
		set {addPDFLinkOpt, theTop, newWidth} to create checkbox "Include PDF files as link in MV" bottom (theTop + 8) max width accViewWidth / 3 - 8 --with initial state 	
		set {boldLabel, theTop} to create label "Merge Markdown, RTF, RTFD files" bottom theTop + 8 max width accViewWidth control size large size
		set allControls to {theRule3, addSeperatorOpt, addNameOpt, addTagOpt, theRule2, sortOrderOpt, sortByOpt, theRule1, gpPopup, selOpt, theRule0, addPDFLinkOpt, boldLabel}
		set {buttonName, controlsResults} to display enhanced window "MergeView" acc view width accViewWidth acc view height theTop acc view controls allControls buttons theButtons with align cancel button
		--  end of "Dialog Toolkit Plus" coding
		
		if buttonName is not "Cancel" then
			return controlsResults
		else
			return buttonName
		end if
		
	on error
		display dialog "Err getMVOpt"
	end try
end getMVOpt
on prepareGpsChoice()
	local lg, lgn, tgu
	set lgn to {}
	tell application id "DNtp"
		try
			set lg to every smart group of current database
			-- set lg to my sortRecsByName(lg, "A")
			set lgre to length of lg
			if lg is not {} then my QsortRecsByName(lg, 1, lgre)
			if name of current group is not equal to name of current database then set the beginning of lg to current group
			
			if lg ≠ {} then
				repeat with each in lg
					set end of lgn to name of each & "      «" & location of each & "»"
				end repeat
				return {lg, lgn}
			else
				return {{}, {"Parent is database and no smart group"}, tgu}
			end if
		on error
			display dialog "Err prepareGpsChoice"
		end try
	end tell
end prepareGpsChoice
on addNotes()
	-- this handler uses global variables in the main program for input
	set theSpacer to "   " -- spacer used to display the group hierarchy in the dialog box
	try
		tell application id "DNtp"
			set lg to my getAllGps(theGps, theSpacer) -- lg holds all items in MV that are group or tag group
			if lg is {} then
				display alert "There is no gorup or only have tags in the MV. Refreshing MV now." giving up after 2
				return
			end if
			set ln to {}
			set idx to 1
			repeat with each in lg
				set end of ln to (idx & "." & each's item 2 as string)
				set idx to idx + 1
			end repeat
			
		end tell
		
		set qNoteGpsName to ln
		
		-- "Dialog Toolkit Plus" coding
		set accViewWidth to 600
		set {theButtons, minWidth} to create buttons {"Cancel", "OK"} default button 2 given «class btns»:2
		if minWidth > accViewWidth then set accViewWidth to minWidth -- make sure buttons fit
		
		set {noteField, noteLabel, theTop} to create top labeled field "" placeholder text "Type filename at the first line." bottom 0 field width accViewWidth extra height 400 label text "Add file:" with accepts linebreak and tab
		set {theRule2, theTop} to create rule (theTop + 12) rule width accViewWidth
		
		set {alarmTextField, alarmTextLabel, theTop, fieldLeft} to create side labeled field "" placeholder text "Type message/email address/url here" bottom (theTop + 8) total width accViewWidth label text "Reminder message:    " field left 0
		
		set {addReminderPopup, theTop} to create popup reminderTypeList left inset 0 bottom (theTop + 8) popup width 160 initial choice 1
		
		set {alarmDateTimeField, theTop} to create field (my getDateString(current date, alarmDateTimeFormat, ".") & " " & my getDateString(current date, "hn", ":")) left inset 170 bottom (theTop - 22) field width accViewWidth - 170
		
		set {theRule1, theTop} to create rule (theTop + 12) rule width accViewWidth
		
		set {openNotePopup, openNoteLabel, theTop} to create labeled matrix {"Save Only", "Save+Open"} bottom (theTop + 8) max width accViewWidth - 300 matrix left 0 label text "Action: " initial choice 1 without arranged vertically
		
		set {fileFormatPopup, fileFormatLabel, theTop} to create labeled matrix {"Markdown", "Rich Text"} bottom (theTop + 8) max width accViewWidth - 300 matrix left 0 label text "Format:" initial choice 1 without arranged vertically
		
		set {locPopup, locLabel, theTop} to create labeled popup qNoteGpsName bottom (theTop + 8) popup width 250 max width accViewWidth label text "Where:  " popup left 0 initial choice 1
		
		set allControls to {noteField, noteLabel, theRule2, alarmTextField, alarmTextLabel, addReminderPopup, alarmDateTimeField, theRule1, openNotePopup, openNoteLabel, fileFormatPopup, fileFormatLabel, locPopup, locLabel}
		
		set {buttonName, controlsResults} to display enhanced window "MergeView - Add Note" acc view width accViewWidth acc view height theTop acc view controls allControls buttons theButtons active field noteField with align cancel button
		--  end of "Dialog Toolkit Plus" coding
		
		-- credit @Bluefrog's snippet
		--get parameters of reminder
		set reminderDateTime to (date (controlsResults's item 7 as string))
		set addReminder to controlsResults's item 6
		set reminderMessage to controlsResults's item 4
		
	on error
		display dialog "Err addNotes 1"
	end try
	
	tell application id "DNtp"
		try
			if buttonName is "OK" then
				set theDate to my getDateString(current date, "ymdhn", ".")
				
				
				if controlsResults's item 1 is not "" then
					-- use 1st line of note as filename	
					set theName to first paragraph of controlsResults's item 1
				else
					-- if user has forgotton to add file name
					set theName to "Created by MV on " & theDate
				end if
				
				--get content of the note
				set theContent to controlsResults's item 1
				-- get the action for save only or save + open
				set openNoteAfterCreation to controlsResults's item 9
				--get the format of the note to be saved
				set fileFormat to controlsResults's item 11
				
				-- get the location to save the note
				-- k is a temporary variable to get the position of group in theGps
				set k to texts 1 thru ((offset of "." in controlsResults's item 13) - 1) of controlsResults's item 13
				set qNoteGp to (item k of lg)'s item 1
				
				
				if fileFormat is "Markdown" then
					
					--check for no entry
					if controlsResults's item 1 is not "" then
						set theContent to every paragraph of theContent
						-- (1) the 1st paragraph is the name of file (2) there are two hiddem return return in the line-belowed
						set theContent to my listToStr(rest of theContent, "
	")
					else
						set theContent to ""
					end if
					
					set theNote to create record with {name:theName, source:theContent, type:markdown} in qNoteGp
				else if fileFormat is "Rich Text" then
					set theNote to create record with {name:theName, source:theContent, type:rtfd} in qNoteGp
				end if
				log message "A file named " & theName & " is created in " & location of qNoteGp
				set unread of theNote to true
				
				-- set reminder
				if addReminder is "Alert" then
					tell theNote to make new reminder with properties {schedule:once, alarm:alert, alarm string:reminderMessage, due date:reminderDateTime}
				else if addReminder is "Notification" then
					tell theNote to make new reminder with properties {schedule:once, alarm:notification, alarm string:reminderMessage, due date:reminderDateTime}
				else if addReminder is "Speak Text" then
					tell theNote to make new reminder with properties {schedule:once, alarm:speak, alarm string:reminderMessage, due date:reminderDateTime}
				else if addReminder is "Send mail" then
					tell theNote to make new reminder with properties {schedule:once, alarm:mail, alarm string:reminderMessage, due date:reminderDateTime}
				else if addReminder is "Launch URL" then
					set the URL of theNote to reminderMessage
					tell theNote to make new reminder with properties {schedule:once, alarm:launch, due date:reminderDateTime}
				end if
				
				if openNoteAfterCreation is "Save+Open" then
					open tab for record theNote
				end if
			end if
			
		on error
			display dialog "Err addNotes 2"
		end try
	end tell
end addNotes
on getAllGps(theseGps, theTab)
	local l
	set l to {}
	tell application id "DNtp"
		try
			set idx to 0
			repeat with each in theseGps
				
				if each's type is group and each's tag type is not ordinary tag then
					set idx to idx + 1
					set end of l to {each's item 1, theTab & ((each's item 1)'s name)}
					set l to l & my getAllGps(children of each, theTab & theTab)
					
				end if
			end repeat
		on error
			display dialog "Err getAllGps"
		end try
	end tell
	return l
end getAllGps
on getAllChildren(theseGps)
	local l
	set l to {}
	tell application id "DNtp"
		repeat with each in theseGps
			if each's type is group or each's class is smart group then
				-- if each's type is group or (each's type as string) is "smart group" then
				set l to l & my getAllChildren(children of each)
			else
				if each's type is in {rtf, rtfd, markdown} then set end of l to each's item 1
			end if
		end repeat
		
	end tell
	return l
end getAllChildren
on QsortRecsByMod(array, leftEnd, rightEnd)
	-- based on Hoare's QuickSort Algorithm
	-- modified by ngan for records sorting in DT
	tell application id "DNtp"
		script a
			property l : array
		end script
		set {i, j} to {leftEnd, rightEnd}
		set v to modification date of item ((leftEnd + rightEnd) div 2) of a's l -- pivot in the middle
		repeat while (j > i)
			repeat while (modification date of (item i of a's l) < v)
				set i to i + 1
			end repeat
			repeat while (modification date of (item j of a's l) > v)
				set j to j - 1
			end repeat
			if (not i > j) then
				tell a's l to set {item i, item j} to {item j, item i} -- swap
				set {i, j} to {i + 1, j - 1}
			end if
		end repeat
		if (leftEnd < j) then my QsortRecsByMod(a's l, leftEnd, j)
		if (rightEnd > i) then my QsortRecsByMod(a's l, i, rightEnd)
	end tell
end QsortRecsByMod
on QsortRecsByName(array, leftEnd, rightEnd)
	-- based on Hoare's QuickSort Algorithm
	-- modified by ngan for records sorting in DT
	tell application id "DNtp"
		script a
			property l : array
		end script
		set {i, j} to {leftEnd, rightEnd}
		set v to name of item ((leftEnd + rightEnd) div 2) of a's l -- pivot in the middle
		repeat while (j > i)
			repeat while (name of (item i of a's l) < v)
				set i to i + 1
			end repeat
			repeat while (name of (item j of a's l) > v)
				set j to j - 1
			end repeat
			if (not i > j) then
				tell a's l to set {item i, item j} to {item j, item i} -- swap
				set {i, j} to {i + 1, j - 1}
			end if
		end repeat
		if (leftEnd < j) then my QsortRecsByName(a's l, leftEnd, j)
		if (rightEnd > i) then my QsortRecsByName(a's l, i, rightEnd)
	end tell
end QsortRecsByName
on QsortRecsByCreate(array, leftEnd, rightEnd)
	-- based on Hoare's QuickSort Algorithm
	-- modified by ngan for records sorting in DT
	tell application id "DNtp"
		script a
			property l : array
		end script
		set {i, j} to {leftEnd, rightEnd}
		set v to creation date of item ((leftEnd + rightEnd) div 2) of a's l -- pivot in the middle
		repeat while (j > i)
			repeat while (creation date of (item i of a's l) < v)
				set i to i + 1
			end repeat
			repeat while (creation date of (item j of a's l) > v)
				set j to j - 1
			end repeat
			if (not i > j) then
				tell a's l to set {item i, item j} to {item j, item i} -- swap
				set {i, j} to {i + 1, j - 1}
			end if
		end repeat
		if (leftEnd < j) then my QsortRecsByCreate(a's l, leftEnd, j)
		if (rightEnd > i) then my QsortRecsByCreate(a's l, i, rightEnd)
	end tell
end QsortRecsByCreate
on getRTFContent(theRTFSource)
	set {AppleScript's text item delimiters, tid} to {"<body>", AppleScript's text item delimiters}
	set theText to text item 2 of theRTFSource
	set AppleScript's text item delimiters to "</body>"
	set theText to text item 1 of theText
	set AppleScript's text item delimiters to tid
	return theText
end getRTFContent

-- utility handlers
on indexOfOneItem(theItem, theList)
	-- credits Emmanuel Levy
	set {oTIDs, AppleScript's text item delimiters} to {AppleScript's text item delimiters, return}
	set theList to return & theList & return
	set AppleScript's text item delimiters to oTIDs
	try
		
		-1 + (count (paragraphs of (text 1 thru (offset of (return & theItem & return) in theList) of theList)))
	on error
		0
	end try
end indexOfOneItem
on getDateString(theDate, theDateFormat, theSeperator)
	tell application id "DNtp"
		local y, m, d, h, n, s, T
		local lol, ds
		set lol to {{"y", ""}, {"m", ""}, {"d", ""}, {"h", ""}, {"n", ""}, {"s", ""}}
		
		set (lol's item 1)'s item 2 to get year of theDate
		set (lol's item 2)'s item 2 to my padNum((get month of theDate as integer) as string, 2)
		set (lol's item 3)'s item 2 to my padNum((get day of theDate) as string, 2)
		set T to every word of (get time string of theDate)
		set (lol's item 4)'s item 2 to T's item 1
		set (lol's item 5)'s item 2 to T's item 2
		set (lol's item 6)'s item 2 to T's item 3
	end tell
	
	set ds to {}
	set theDateFormat to (every character of theDateFormat)
	repeat with each in theDateFormat
		set ds to ds & (my lolLookup(each as string, 1, 2, lol))'s item 2
	end repeat
	
	return my listToStr(ds, theSeperator)
	
end getDateString
on padNum(lngNum, lngDigits)
	-- Credit houthakker
	set strNum to lngNum as string
	set lngGap to (lngDigits - (length of strNum))
	repeat while lngGap > 0
		set strNum to "0" & strNum
		set lngGap to lngGap - 1
	end repeat
	strNum
end padNum
on lolLookup(lookupVal, lookUpPos, getValPos, theList)
	--only for list of list with more than 1 items
	local i, j, k
	set j to lookUpPos
	set k to getValPos
	repeat with i from 1 to length of theList
		if (item j of item i of theList) is equal to lookupVal then return {i, item k of item i of theList, item i of theList}
	end repeat
	return {0, {}, {}}
end lolLookup
on findAndReplaceInText(theText, theSearchString, theReplacementString)
	set AppleScript's text item delimiters to theSearchString
	set theTextItems to every text item of theText
	set AppleScript's text item delimiters to theReplacementString
	set theText to theTextItems as string
	set AppleScript's text item delimiters to ""
	return theText
end findAndReplaceInText
on listToStr(theList, d)
	local thestr
	set {tid, text item delimiters} to {text item delimiters, d}
	set thestr to theList as text
	set text item delimiters to tid
	return thestr
end listToStr
on strToList(thestr, d)
	local theList
	set {tid, text item delimiters} to {text item delimiters, d}
	set theList to every text item of thestr
	set text item delimiters to tid
	return theList
end strToList
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

zip file of the script
MergeViewV1b13(Final).scpt.zip (101.2 KB)

1 Like

Thanks a lot for this addition @ngan!

Directly from the script editor it works. When I run it from DT, I get the error: “Err Main 7”

An updated version is posted here.

1 Like

I’ll freeze the design of the MV script (v1b13). However:

  • If anyone comes up with good ideas, please modify the script as you wish or make suggestions, such as @Bernardo_V to add randomize footnote to potentially turn MV into a writing tool (very smart!), or @Eds who make a good suggestion to add the file’s parents in each section (very useful for me, too). Please post the modification to the forum for sharing.
  • If anyone sees an error message while running the script (such as “Err Main 5”), the messages is one of the many errors checking points in the script. Please let me know about it and I’ll try my best to fix the bugs since I foresee myself be using the MV regularly.

The MV is more fun and useful than I have expected. My next proof-of-concept task is to use the core elements of the MV to build a simple but dynamic[almost] MVOuliner script.

Cheers

2 Likes