I owe many ideas and pieces of codes from @korm, @Frederiko, @bluefrog, @cgrunenberg and many other members in finishing this very interesting script (I think). IMHO, the most genuine inspirations are coming from Make an Annotation with Links, Notes, Tags v2 and Annotation Pane (Annotation with Links, Notes, Tags v3).
Two things are in my mind before I start this project: (1) DT3 has dramatically and simultaneously increased both the ease of use as well as the level of customisation (2) I admire v2 and V3, but I feel that the scripts are not giving enough flexibility in configuration (but it’s gracefully expected coz they are likely personalised version with lots of additional efforts to help other DT users to use them)- some users like to structure their ideas before writing, some users like to do it the other way round. So, I want to take the challenge to utilise some of the new features of DT3, and provide a script that can allow a very disciplined or very flexible way for building up multiple notes/cited text/snippets form each source/annotated documents.
EDITED on 4 July to reflect the changes in V0.90
(* Two Main Purposes*)
– (1) To cite a piece of selected text from pdf or text file and take notes in one card (snippet). You can create many cards for different snippets. Each card can be tagged and comment individually. All cards from the same document are gathered in one document-stack. All document-stacks are gathered in one topstack.
– (2) To create multiple notes (with no cited text) on the same document, and all notes from the same document are gathered in one docuement-stack. All docuement-stacks are gathered in one topstack.
(* Features *)
– two options of topstack: (opt1) each database has one topstack to gather all document-stacks within the database (opt2) one topstack to gather all document-stacks from all databases
– for (opt1): You need to create a group name “Stack” at the root of each database that you want to collect muliple notes and snippets.
– for (opt2): For advance user who knows how to extract the uuid of a group. The centralised topstack can be placed in anywhere in a database, in any database, and by any name you choose. You can change the name and relocate the top stack anytime WARNING: however, frequent chnage is discouraged.
– you can track how many notes/snippets were created at inspector bar of the source document
– you can call up the whole document-stack of notes/snippets from the inspector bar of the source document
– cited text will be highlighted in the source docuement, at a colour specifed by you, or by underlying.
– you can create as many linked notes on the same document as you want.
– there are many ways to tag each snippets/cards. e.g, none, manual input at time of card-creation, inherit all or some of the tags from the source, and selection of multiple tags all at once from the entire database or from a preset list of tag (constrained list).
– the card can be named by the full name or the aliases of the source document, followed by an unique identifer
– you can choose to be asked, or not, to store a very short summary of each card to the comment field at the time of card-creation. This may help you to recall what are those cards about.
– you can pre-set the font type of the note.
– you can preset where on the screen the newly created note is placed when it is first created.
– Back-link is based on page-link in pdf, and just backlink for text file. However, paragraph link for text file will be possible when DT3 final version is out (I hope)
(* Notes *)
– topstack (opt1) is easier for topstack setup, and the only option if you need to have different topstack for each database.
– topstack (opt2) is only for those advance users who know scripting, and really want to have a centralised topstack.
– You need to know basic scripting to setup the stack and constrained tag list and to configure all of the above functions.
– the Script is written with many handlers, easier for debugging and for customisation BUT,
– for beginners, handlers can be confusing, because variables used by main program and handlers have many similar but not identical names
– I did quite a lot of debugging, but can’t guarantee every options will work, use it at ur own risk
– I might not be able to answer your question (my holiday is over), I develop this script based on personal need but trying my best to make it more approachable to for other users and for different usages.
– I release the code beacuse I learn so much by reading from this forum and I think codes should be shared among each others. You are welcome to take control of the script and modify it.
– Don’t rush to use the script, study the (SET UP) in the script carefully. Different sections of codes will look familar to those experienced members. But I try to code as clean as possible.
– IMHO, it’s best to use 3rd debugger for viewing and customising the script, it’s rather long.
– I understand that there are also script/s to import paper-group of notes from bookend. This script could be an integration to such structure. But I’ll leave it to others as I don’t use bookend.
The most important setup:
Option 1: Use one topstack in each database (should be easier, I hope)
(1) Create a group named “Stack” in the root of the database.
(2) Create tag named “ParentTag” in the root of the Tags of the database. However, you may not need to use it.
Option 2: use one centralised topstack for all databases (for very experienced users only)
(1) You can create the group “Stack” and the tag “ParentTag” anywhere. But you need to extract the UUID of both items for later use. Read the script for details.
Common to Option1 and 2
(3) Create two custom meta data fields.
one for linking to the stack, another to keep track of the number of cards that were created in each document-stack. If u don’t touch that field, each note will have an unique identifier. Of course, you can always reset the count after the testing phase.
A friendly reminder:
Test the script with a test database.
If you are uncomfortable with the script, IMHO the best thing is not to use it right away. Try taking time to read more forum posts about scripts, try running simple scripts as a start, gain experience, and perhaps going back to more complicated script including this script (that’s what I did). If this feature has real application value, someone will/may modify it either from the ease-of-setup and configuration or from feature perspectives
Check out my other posts in this thread, now I know how hard a job @bluefrog is having!
(* SET UP*)
-- Option for Stacks and ParentTag.
property byLocation : true -- "true" for opt1 (one topstack-one database), "false" for opt2 (one topstack- many database)
-- For opt1 - if byLocation : true
-- user should create a group "Stack" at the root of the database,
property stackLocation : "/Stack" -- don't touch
-- user only need to create a ParentTag if they need to use constrained tag list
-- user should create a tag "ParentTag" at the root of Tags and move all tags that you may want to use for constrained tagging under "ParentTag"
property parentTagLocation : "/Tags/ParentTag" -- don't tocuh
-- WARNING: only for very experienced user
--- For opt2 - if byLocation : false
-- the name and location of group don't matter as long as absTopStackUUID is correct
property absTopStackUUID : "7D11E68B-AA65-4465-98E8-BFD7CA1A8D23" -- please set this property to your centralised topstack's uuid
-- the name and location of parentTag don't matter as long as absParentTagUUID is correct, the constrained list will be the same for all databases
property absParentTagUUID : "160F4034-CA47-4A77-A415-C335E5FC2D12" -- please set this property to centralised your parentTag's uuid
-- You need to create two custom meta data fields to hold the location of document-stack and keep track of the number of cards to create an unique card identifier
set mdCardNum to "mdcardnum" as string -- data type is "Integer", keep track of the number of cards created in each document-stack
set mdStackLink to "mdstacklink" as string -- data type is "URL", hold the link to the document-stack
-- Type, naming, format, numerate of card
-- IniCardType : "C" is more flexible, if do have not selected any text, the script will ask you whether to create blank-note or to quit
property IniCardType : "C" -- "C" for creating note with cited text or blank note, "N" for blank note only
property cardNameFormat : "F" -- "F" card name = full name of the annotated document + unique card ID, "A" = aliases + + unique card ID
property cardCountDig : 3 -- 3 digits = from 001 to 999 cards for each document
property fontOfCard : "<font face=\"Helvetica\">" -- Font to use in the card
-- property fontOfCard : "<font face=\"SF Pro Text\" style=\"font-weight:100;\">". -- my personal font face
property addCardComment : true -- true if user want to add short comment to the Finder comment.
-- Tagging
global theTags -- don't touch
global cardType -- don't touch
property cardTagging : "A" -- Tagging methods: "N"= None, "M"=Manually input, "C1"=Constrained to one level tagsunder one parent tag, "CA"= Constrained to to all level tags under one parent tag "A"=All tags in database, "IA"=Inherit All, "IC"=Inherit by choice
-- Misc Settings
set maxLen to 2000 -- as you wish but I don't know the consequence
property noDecoration : "\" style=\"text-decoration:none;" -- don't touch
-- you can ignore the belowed settings if you don't care where the card window is
global screenWidth, screenHeight -- don't touch
set {screenWidth, screenHeight} to {3200, 1800} -- set to ur main screen's resolution
property cardPosition : "UL" -- placement of newly created card. UL=upper left, ML=middle left, LL=lower left, UM/MM/LM/UR/MR/LR etc. "N" = not specific placement
(*MAIN SCRIPT*)
tell application id "DNtp"
try
set cardType to IniCardType
set thisItem to the first item of (the selection as list)
if thisItem is {} then error "Please select something"
set theCitedText to selected text of think window 1 as string
if theCitedText is "" then
display dialog "No text is selected, change to note-only card? OK to continue, Cancel to quit" buttons {"OK", "Cancel"} default button 1
set cardType to "N"
end if
set TopStackUUID to my getUUID(byLocation, "S")
set ParentTagUUID to my getUUID(byLocation, "T")
set topStack to (get record with uuid TopStackUUID)
set topStackname to (name of topStack)
set theDoc to content record of think window 1
set theDocUUID to (uuid of theDoc) as string
set theDocName to (the name of theDoc)
set theDocAliases to (the aliases of theDoc)
set theStackName to theDocName & " (Stack)"
-- Create cited-text note or plain-note
if cardType is "C" then
if length of theCitedText is greater than maxLen then error "That selection is longer than " & maxLen
-- highlight or underline the selected text
tell application "System Events"
delay 0.05
-- highlight colour number 7, use a specific color to show that the text is cited in the cards, the short-cut to be set in macOS is shift-ctrl-7, or any shortcut at ur choice
-- but you need to know how to do it
key code 26 using {shift down, control down}
-- keystroke "u" using {command down} -- it's easier to use this if you choose to underline the cited text
end tell
-- to remove the symbol "- ", it is common for older journal articles to have many trancated word in sentences' return3
-- comment this chunk of code if you don't need the trim
set AppleScript's text item delimiters to the "- "
set the item_list to every text item of theCitedText
set AppleScript's text item delimiters to the ""
set theCitedText to the item_list as string
set AppleScript's text item delimiters to ""
else
set theCitedText to "Start writing"
end if
-- update the card number, get the location of document-stack if exist, create a new document-stack for 1st time creation
tell theDoc
set MDCustom to the custom meta data of theDoc
try
set theCardNum to mdCardNum of MDCustom
set theCardNum to theCardNum + 1
add custom meta data theCardNum for mdCardNum to theDoc
on error
set theCardNum to 1
add custom meta data theCardNum for mdCardNum to theDoc
end try
try
set theStackLink to mdStackLink of MDCustom
set theStackUUID to texts 21 thru -1 of theStackLink
set theStack to (get record with uuid theStackUUID)
on error
set theStack to create record with {name:theStackName, type:group} in topStack
set theStackUUID to (get uuid of theStack)
add custom meta data "x-devonthink-item://" & theStackUUID for mdStackLink to theDoc
display alert "A new stack for this document is created" giving up after 1
end try
end tell
-- set the name and card identifier for new card
set theCardName to my setCardName(theDoc, theCardNum, cardNameFormat, cardCountDig, cardType)
-- format the text in new card
set theCardContent to my setCitationFormat(theDoc, theCitedText, cardType, think window 1)
-- create the card
set theNewCard to create record with {name:theCardName, source:theCardContent, type:rtf} in theStack
-- ask for comment if the option is enabled in (SET UP)
if addCardComment is true then
set theCardComment to text returned of (display dialog "Add 10 words or less to the comment field:" with title "Comment" default answer "")
my newCardComment(theNewCard, theCardComment)
end if
-- open the card and place it at specfic location on the screen
open window for record theNewCard
my newCardPosition(cardPosition)
-- ask for tagging, methods depends on the option enabled in (SET UP)
my newCardTag(theDoc, theNewCard, cardTagging, ParentTagUUID)
on error error_message number error_number
if the error_number is not -128 then display alert "DEVONthink Pro" message error_message as warning
end try
end tell
(*ALL HANDLERS*)
on setCardName(theDoc, theCardNum, theNameFormat, theCounter, theType)
local theCardName, theDate
set theDate to do shell script "date +'%Y.%m.%d'"
if theType = "C" then
set theCode to ".C"
else
set theCode to ".N"
end if
tell application id "DNtp"
if theNameFormat is "F" then
set theCardName to (name of theDoc) & " (Card " & theDate & theCode & my add_leading_zeros(theCardNum, theCounter - 1) & ")"
else
set theCardName to (aliases of theDoc) & " (Card " & theDate & theCode & my add_leading_zeros(theCardNum, theCounter - 1) & ")"
end if
end tell
return theCardName
end setCardName
on add_leading_zeros(this_number, max_leading_zeros)
set the threshold_number to (10 ^ max_leading_zeros) as integer
if this_number is less than the threshold_number then
set the leading_zeros to ""
set the digit_count to the length of ((this_number div 1) as string)
set the character_count to (max_leading_zeros + 1) - digit_count
repeat character_count times
set the leading_zeros to (the leading_zeros & "0") as string
end repeat
return (leading_zeros & (this_number as text)) as string
else
return this_number as text
end if
end add_leading_zeros
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 setCitationFormat(theRecord, theText, theCardType, theWin)
tell application id "DNtp"
try
-- set thePage to the ((current page of think window 1) as string)
if the kind of theRecord is in {"PDF+Text"} then
set thePage to (current page of theWin)
set theLink to (the reference URL of theRecord) & "?page=" & (thePage)
set thePageLink to "<a href=\"" & theLink & noDecoration & "\">" & " Back-Link to pdf page " & (thePage + 1) & "</a>"
set thePageString to " (:" & (thePage + 1) & ")"
else if the kind of theRecord is not {"PDF+Text"} then
-- wait for DT3v4!
-- set theLink to (the reference URL of theRecord) & "?paragraph=" & theCitation
set theLink to (the reference URL of theRecord)
set thePageLink to " <a href=\"" & theLink & noDecoration & "\"> Back-Link to text paragraph source </a>"
set thePageString to ""
end if
if theCardType = "C" then
set theAnnotation to "<meta charset=\"UTF-8\">" & fontOfCard & "<p><b>" & thePageLink & "</b></p>" & "<p>" & "<b>Cited Text:</b><br>" & theText & "<br><b>" & thePageString & "</b></p>" & "<b>Notes:</b><br></font>"
else if theCardType = "N" then
-- set theAnnotation to "<meta charset=\"UTF-8\">" & fontOfCard & "<p><b>" & thePageLink & "</b></p><b>" & thePageString & "</b></p><b>Notes:</b><br></font>"
-- set theAnnotation to "<meta charset=\"UTF-8\">" & fontOfCard & "<p><b>" & thePageLink & "</b></p><b>" & thePageString & "</b></p><b>Notes:</b><br>" & theText & "</font>"
set theAnnotation to "<meta charset=\"UTF-8\">" & fontOfCard & "<p><b>" & thePageLink & "</b></p><b>Notes related to:</b><br>" & theText & "</font>"
end if
-- from (F)
-- set o_theAnnotation to do shell script "echo " & theAnnotation & " | textutil -format html -convert rtf -stdin -stdout | pbcopy -Prefer rtf"
-- from (k)
set o_theAnnotation to (do shell script "echo " & quoted form of theAnnotation & " | textutil -format html -convert rtf -stdin -stdout")
return o_theAnnotation
on error error_message number error_number
if the error_number is not -128 then display alert "setCitationFormat() has error" message error_message as warning
end try
end tell
end setCitationFormat
on newCardPosition(thePosition)
tell application id "DNtp"
try
set {WinWidth, WinHeight} to {screenWidth / 3, (screenHeight - 22) / 3} -- newly opened main window sized as a quarter of the screen
set yAxis to 22
set xAxis to 0
if thePosition is "UL" then set bounds of window 1 to {xAxis, yAxis, xAxis + WinWidth, yAxis + WinHeight}
if thePosition is "ML" then set bounds of window 1 to {xAxis, yAxis + WinHeight, xAxis + WinWidth, yAxis + 2 * WinHeight}
if thePosition is "LL" then set bounds of window 1 to {xAxis, yAxis + 2 * WinHeight, xAxis + WinWidth, yAxis + 3 * WinHeight}
if thePosition is "UM" then set bounds of window 1 to {xAxis + WinWidth, yAxis, xAxis + 2 * WinWidth, yAxis + WinHeight}
if thePosition is "MM" then set bounds of window 1 to {xAxis + WinWidth, yAxis + WinHeight, xAxis + 2 * WinWidth, yAxis + 2 * WinHeight}
if thePosition is "LM" then set bounds of window 1 to {xAxis + WinWidth, yAxis + 2 * WinHeight, xAxis + 2 * WinWidth, yAxis + 3 * WinHeight}
if thePosition is "UR" then set bounds of window 1 to {xAxis + 2 * WinWidth, yAxis, xAxis + 3 * WinWidth, yAxis + WinHeight}
if thePosition is "MR" then set bounds of window 1 to {xAxis + 2 * WinWidth, yAxis + WinHeight, xAxis + 3 * WinWidth, yAxis + 2 * WinHeight}
if thePosition is "LR" then set bounds of window 1 to {xAxis + 2 * WinWidth, yAxis + 2 * WinHeight, xAxis + 3 * WinWidth, yAxis + 3 * WinHeight}
on error error_message number error_number
if the error_number is not -128 then display alert "NewCardPosition() has error" message error_message as warning
end try
end tell
end newCardPosition
on newCardComment(theRecord, theComment)
tell application id "DNtp"
try
set comment of theRecord to theComment
on error error_message number error_number
if the error_number is not -128 then display alert "newCardComment() has error" message error_message as warning
end try
end tell
end newCardComment
on newCardTag(theSourceDoc, theRecord, theTagType, theTagUUID)
local theTags
tell application id "DNtp"
set parentTag to (get record with uuid theTagUUID)
try
if theTagType is "M" then
repeat
set theTagList to display name editor "Add Tags" info "Tags (separated by comma):"
if theTagList is not "" then exit repeat
end repeat
else if theTagType is "IA" then
set theTagList to tags of theSourceDoc
else if theTagType is "IC" then
set theTags to tags of theSourceDoc
if theTags is {} then
display alert "Your source document does not have any tag" giving up after 1
set theTagList to {}
else
set theSortTags to my sortlist(theTags)
set theTagList to (choose from list theSortTags with prompt {"Choose tags to apply:"} default items "" with multiple selections allowed)
end if
else if theTagType is "C1" then
set theTags to (name of every child of parentTag) whose (tag type is ordinary tag)
set theSortTags to my sortlist(theTags)
set theTagList to (choose from list theSortTags with prompt {"Choose tags to apply:"} default items "" with multiple selections allowed)
else if theTagType is "CA" then
set theTags to my gettags(parentTag)
set theSortTags to my sortlist(theTags)
set theTagList to (choose from list theSortTags with prompt {"Choose tags to apply:"} default items "" with multiple selections allowed)
else if theTagType is "A" then -- could be slow if db has many tags
set theTags to (name of every parent of current database whose (tag type is ordinary tag))
set theSortTags to my sortlist(theTags)
set theTagList to (choose from list theSortTags with prompt {"Choose tags to apply:"} default items "" with multiple selections allowed)
else
set theTagList to {}
end if
set the tags of theRecord to theTagList
on error error_message number error_number
if the error_number is not -128 then display alert "newCardTag() has error" message error_message as warning
end try
end tell
end newCardTag
on gettags(theParent)
local theTagList, tagName
set theTagList to {}
tell application id "DNtp"
repeat with theChild in children of theParent
if tag type of theChild is ordinary tag then
set tagName to the name of theChild
set theTagList to theTagList & tagName
set theTagList to theTagList & my gettags(theChild)
end if
end repeat
end tell
return theTagList
end gettags
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 getUUID(thePathType, whichUUID)
tell application id "DNtp"
local theUUID, theRecord
try
if thePathType is true then
if whichUUID is "S" then
set theRecord to (get record at stackLocation)
set theUUID to (get uuid of theRecord)
else if whichUUID is "T" then
set theRecord to (get record at parentTagLocation)
set theUUID to (get uuid of theRecord)
end if
else
if whichUUID is "S" then
set theRecord to (get record with uuid absTopStackUUID)
set theUUID to (get uuid of theRecord)
else if whichUUID is "T" then
set theRecord to (get record with uuid absParentTagUUID)
set theUUID to (get uuid of theRecord)
end if
end if
return theUUID
on error error_message number error_number
if the error_number is not -128 then display alert "getUUID() has error" message error_message as warning
end try
end tell
end getUUID