├── .gitignore ├── Bookends └── Update_group_publications_from_attachment_name │ ├── README.md │ ├── Update_group_publications_from_attachment_name.app.zip │ ├── Update_group_publications_from_attachment_name.applescript │ └── Update_group_publications_from_attachment_name.scpt ├── DEVONthink └── DEVONthink_Notes_from_PDF_Annotations │ ├── DEVONthink_Notes_from_PDF_Annotations.applescript │ ├── DEVONthink_Notes_from_PDF_Annotations.scpt │ ├── DEVONthink_Notes_from_PDF_Annotations.scptd.zip │ └── README.md ├── LICENSE ├── Papers3 ├── Papers_To_Bookends │ ├── Papers_To_Bookends.app.zip │ ├── Papers_To_Bookends.applescript │ ├── Papers_To_Bookends.scpt │ └── README.md └── Papers_To_DEVONthink │ ├── Papers_To_DEVONthink.app.zip │ ├── Papers_To_DEVONthink.applescript │ ├── Papers_To_DEVONthink.scpt │ └── README.md ├── README.md ├── ScriptingLibraries └── KeypointsScriptingLib │ ├── KeypointsScriptingLib.applescript │ ├── KeypointsScriptingLib.scptd.zip │ └── README.md └── docs ├── Bookends ├── Getting_Started.applescript ├── Getting_Started.md ├── Getting_Started.scpt └── Images │ ├── ScriptingBookends-AttachmentItem-Properties.png │ ├── ScriptingBookends-BookendsDictionary-ScriptDebugger.png │ ├── ScriptingBookends-BookendsDictionary-ScriptEditor.png │ ├── ScriptingBookends-ContainerHierarchy.png │ ├── ScriptingBookends-Inheritance.png │ ├── ScriptingBookends-LibraryWindow-Elements.png │ ├── ScriptingBookends-LibraryWindow-Properties.png │ ├── ScriptingBookends-PublicationItem-Properties.png │ └── ScriptingBookends-Publications-ByGroup.png ├── Papers3 ├── Getting_Started.md └── Images │ ├── ScriptingPapers-ContainerHierarchy.png │ ├── ScriptingPapers-Inheritance.png │ ├── ScriptingPapers-KeywordItem-Properties.png │ ├── ScriptingPapers-LibraryWindow-Properties.png │ ├── ScriptingPapers-PapersDictionary.png │ ├── ScriptingPapers-PersonItem-Properties.png │ ├── ScriptingPapers-PublicationItem-Elements.png │ └── ScriptingPapers-PublicationItem-Properties.png └── _config.yml /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *GITIGNORE* 3 | 4 | # Executables 5 | *.exe 6 | *.out 7 | *.app 8 | *.i*86 9 | *.x86_64 10 | *.hex 11 | 12 | # Compiled bundles 13 | *.scptd 14 | -------------------------------------------------------------------------------- /Bookends/Update_group_publications_from_attachment_name/README.md: -------------------------------------------------------------------------------- 1 | # Update group publications from attachment name 2 | 3 | This sample script for Bookends 13.2 or greater shows how to extract information from attachment file names, and set publication metadata accordingly: 4 | 5 | For all publications contained in the group chosen from your frontmost Bookends library window, this script will take the first attachment of each publication, extract the part of the attachment name that's enclosed with parentheses, and add all of its words as individual publication keywords. 6 | 7 | As an example, consider a PDF file name for an academic paper (e.g.: http://dx.doi.org/10.1038/npp.2015.181) where the user has added his own keywords within parentheses to the file name, like this: 8 | 9 | Chen & Baram '16 (dev stress cognitive review).pdf 10 | 11 | This script will extract those keywords ("dev", "stress", "cognitive", and "review") and set the publication's "Keywords" field accordingly. 12 | 13 | For more info on how to adopt this script to your needs, please see the comments above the script's source code. 14 | 15 | 16 | ## Installation 17 | 18 | The precompiled & signed `.app` version of this script is ready to go. Just [download](https://github.com/extracts/mac-scripting/raw/master/Bookends/Update_group_publications_from_attachment_name/Update_group_publications_from_attachment_name.app.zip) the zipped `.app` package, then double click it to unzip. 19 | 20 | 21 | ## Usage 22 | 23 | Before executing the app, make sure that Bookends is running. Then double click the script. This will present a dialog with a list of all user-created groups from your frontmost Bookends library window. Please select the group that contains the publications you'd like to process, then press the "Run" button. 24 | 25 | Note that the keywords which were extracted from attachment names will be _appended_ the the "Keywords" field of the corresponding publications. If you'd rather like the current contents of a publication's "Keywords" field get _replaced_ instead, open the script in "Script Editor" app and set its `replaceExistingKeywords` property to `true`. Then compile, save and run the script again. 26 | 27 | 28 | ## Requirements 29 | 30 | This script requires macOS 10.10 (Yosemite) or greater, and [Bookends](http://www.sonnysoftware.com/) 13.2 or greater. 31 | 32 | 33 | ## Credits 34 | 35 | by Matthias Steffens, keypointsapp.net, mat(at)extracts(dot)de 36 | 37 | 38 | ## License 39 | 40 | This script is licensed under the MIT license. In short, you can do whatever you want with this script as long as you include the original copyright and license notice in any copy of the script/source. 41 | 42 | For more info, please see [MIT license](https://github.com/extracts/mac-scripting/blob/master/LICENSE). 43 | 44 | 45 | ## Release Notes 46 | 47 | ### v1.0 48 | 49 | Initial release. 50 | -------------------------------------------------------------------------------- /Bookends/Update_group_publications_from_attachment_name/Update_group_publications_from_attachment_name.app.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/extracts/mac-scripting/07649f2dadc592a68048ec88c79e31e6037eefac/Bookends/Update_group_publications_from_attachment_name/Update_group_publications_from_attachment_name.app.zip -------------------------------------------------------------------------------- /Bookends/Update_group_publications_from_attachment_name/Update_group_publications_from_attachment_name.applescript: -------------------------------------------------------------------------------- 1 | -- Update group publications from attachment name 2 | -- version 1.0, licensed under the MIT license 3 | 4 | -- by Matthias Steffens, keypoints.app, mat(at)extracts(dot)de 5 | 6 | -- This sample script for Bookends 13.2 or greater shows how to extract information from attachment 7 | -- file names, and set publication metadata accordingly: 8 | 9 | -- For all publications contained in the group chosen from your frontmost Bookends library window, 10 | -- this script will take the first attachment of each publication, extract the part of the attachment 11 | -- name that's enclosed with parentheses, and add all of its words as individual publication keywords. 12 | 13 | -- As an example, consider a PDF file name for an academic paper (e.g.: http://dx.doi.org/10.1038/npp.2015.181) 14 | -- where the user has added his own keywords within parentheses to the file name, like this: 15 | -- 16 | -- Chen & Baram '16 (dev stress cognitive review).pdf 17 | -- 18 | -- This script will extract those keywords ("dev", "stress", "cognitive", and "review") and set the 19 | -- publication's "Keywords" field accordingly. 20 | 21 | -- To adopt this script to your needs, you'd need to set the value of the `substringRegex` property 22 | -- to a search pattern that matches your data in the attachment file name. Also, you may need to 23 | -- edit the value of the `delimiter` property. And in the main script handler (`on run`), instead of 24 | -- calling `setKeywordsForPublication()`, perform your own action to handle the extracted data. 25 | 26 | -- This script requires macOS 10.10 (Yosemite) or greater, and Bookends 13.2 or greater. 27 | 28 | -- TODO: display progress info 29 | 30 | -- ----------- you may edit the values of the properties below ------------------- 31 | 32 | -- The group name whose publications & attachments will be processed by this script. 33 | -- Note that you don't need to specify a group name here, since the script will ask you 34 | -- to choose a group name from a list of all user-created groups. 35 | property groupName : "" 36 | 37 | -- The search pattern (as an ICU-compatible regular expression) which matches the substring 38 | -- that shall be extracted from attachment file names. 39 | -- The given pattern matches everything between the first pair of parentheses. 40 | property substringRegex : "(?<=\\().+?(?=\\))" 41 | 42 | -- Specifies the delimiter that will be used to split the matched substring into individual items. 43 | -- Specify an empty string to avoid any splitting. 44 | property delimiter : " " 45 | 46 | -- Specifies whether the extracted keywords will replace the current contents of the publication's 47 | -- "Keywords" field (`true`), or if the extracted keywords will be appended to any existing keywords 48 | -- (`false`). 49 | property replaceExistingKeywords : false 50 | 51 | -- Specifies the help messages that are displayed when the script is run 52 | property helpMessage : "For all publications in a group, this script will extract all words within the first pair of parens from the first attachment name" 53 | property additionalInfoAppend : ", and append them to the \"Keywords\" field." -- if replaceExistingKeywords is false 54 | property additionalInfoReplace : ", and set the \"Keywords\" field accordingly." -- if replaceExistingKeywords is true 55 | 56 | -- ----------- usually, you don't need to edit anything below this line ----------- 57 | 58 | use AppleScript version "2.4" -- Yosemite (10.10) or later 59 | use scripting additions 60 | 61 | -- main script handler 62 | on run 63 | set chosenGroupNames to my chooseGroupName() 64 | if chosenGroupNames is false then error number -128 -- abort if user pressed "Cancel" 65 | set groupName to first item of chosenGroupNames 66 | 67 | set groupPubs to my publicationsForGroupByName(groupName) 68 | if groupPubs is {} then 69 | set theMessage to "The selected group (\"" & groupName & "\") contains no publications." 70 | set additionalInfo to "Please select a group containing the publications you'd like to process, then run this script again." 71 | my displayMessage("No publications found!", theMessage, additionalInfo, "Try again", "com.sonnysoftware.bookends", 2) 72 | run -- run main script handler again 73 | end if 74 | 75 | repeat with aPub in groupPubs 76 | set attachmentName to my firstAttachmentNameFromPublication(aPub) 77 | if attachmentName is not "" then 78 | set keywordsList to my substringsFromString(attachmentName, substringRegex, delimiter) 79 | if keywordsList is not {} then 80 | set success to my setKeywordsForPublication(aPub, keywordsList) 81 | end if 82 | end if 83 | end repeat 84 | end run 85 | 86 | -- ------------------------------------------------------------------------------------ 87 | 88 | -- Presents a dialog with a list of user-created groups from the frontmost Bookends 89 | -- library window, and let's you choose one of them for further processing. Returns 90 | -- the chosen group name as a list, or `false` if the "Cancel" button was pressed. 91 | on chooseGroupName() 92 | tell application "Bookends" 93 | tell front library window 94 | set userGroups to name of every group item 95 | 96 | set additionalInfo to additionalInfoAppend 97 | if replaceExistingKeywords then 98 | set additionalInfo to additionalInfoReplace 99 | end if 100 | set theMessage to helpMessage & additionalInfo & linefeed & linefeed & "Select group:" 101 | 102 | set chosenGroupNames to choose from list userGroups with title "Please choose a group…" with prompt theMessage default items {groupName} OK button name "Run" cancel button name "Cancel" without multiple selections allowed and empty selection allowed 103 | return chosenGroupNames 104 | end tell 105 | end tell 106 | end chooseGroupName 107 | 108 | -- Displays the given message & additional info in a dialog inside the application specified 109 | -- by the given bundle identifier. If there's no application with that bundle identifier, the 110 | -- message will be displayed within the current application. 111 | on displayMessage(theTitle, theMessage, additionalInfo, buttonName, appBundleID, iconID) 112 | if theTitle is missing value or theTitle is "" then set theTitle to "About this script" 113 | if theMessage is missing value then set theMessage to "" 114 | if buttonName is missing value or buttonName is "" then set buttonName to "OK" 115 | if iconID is not in {0, 1, 2} then set iconID to 1 -- 0: stop, 1: note, 2: caution 116 | 117 | if additionalInfo is not missing value and additionalInfo is not "" then 118 | set separator to "" 119 | if theMessage is not "" then 120 | set separator to linefeed & linefeed 121 | end if 122 | set theMessage to theMessage & separator & additionalInfo 123 | end if 124 | 125 | try 126 | tell application id appBundleID 127 | display dialog theMessage with title theTitle with icon iconID buttons {buttonName} default button buttonName 128 | end tell 129 | on error 130 | tell current application 131 | display dialog theMessage with title theTitle with icon iconID buttons {buttonName} default button buttonName 132 | end tell 133 | end try 134 | end displayMessage 135 | 136 | -- For the frontmost library window opened in Bookends, returns all publications 137 | -- contained in the group of the given name. Returns an empty list if there's no 138 | -- group with that name, or if the group has no publications. 139 | on publicationsForGroupByName(aGroupName) 140 | if aGroupName is missing value or aGroupName is "" then return {} 141 | 142 | tell application "Bookends" 143 | tell front library window 144 | set groupPubs to {} 145 | set matchingGroups to every group item whose name is aGroupName 146 | if matchingGroups is not {} then 147 | set aGroup to first item of matchingGroups 148 | set groupPubs to publication items of aGroup 149 | end if 150 | return groupPubs 151 | end tell 152 | end tell 153 | end publicationsForGroupByName 154 | 155 | -- For the given Bookends publication, returns the name of its first attachment 156 | -- (if there's any), otherwise returns an empty string. 157 | on firstAttachmentNameFromPublication(aPublication) 158 | if aPublication is missing value then return "" 159 | 160 | tell application "Bookends" 161 | set pubAttachments to attachment items of aPublication 162 | if pubAttachments is {} then return "" 163 | 164 | set anAttachment to first item of pubAttachments 165 | set attachmentName to name of anAttachment 166 | return attachmentName 167 | end tell 168 | end firstAttachmentNameFromPublication 169 | 170 | -- Sets the "Keywords" field of the given Bookends publication to the given 171 | -- list of keywords. Returns true if the keywords could be set successfully, 172 | -- otherwise returns false. 173 | on setKeywordsForPublication(aPublication, keywordsList) 174 | if aPublication is missing value then return false 175 | if keywordsList is missing value or keywordsList is {} then return false 176 | 177 | set keywordsString to my mergeTextItems(keywordsList, linefeed) 178 | tell application "Bookends" 179 | if (not replaceExistingKeywords) then 180 | set existingKeywords to keywords of aPublication 181 | if existingKeywords is not "" then 182 | set keywordsString to existingKeywords & linefeed & keywordsString 183 | end if 184 | end if 185 | set keywords of aPublication to keywordsString 186 | end tell 187 | return true 188 | end setKeywordsForPublication 189 | 190 | -- Extracts the first substring matched by the Regex given in `searchPattern` 191 | -- from `aString`. If `splitDelim` is empty, returns the matched substring as 192 | -- a list. If `splitDelim` is non-empty, the matched substring will be further 193 | -- split by this delimiter, and the resulting list of items will be returned. 194 | -- Returns an empty list if nothing was matched. 195 | on substringsFromString(aString, searchPattern, splitDelim) 196 | if aString is missing value or aString is "" then return {} 197 | if searchPattern is missing value or searchPattern is "" then return {} 198 | 199 | set substring to my regexMatch(aString, searchPattern) 200 | if substring is not "" then 201 | set substrings to my splitText(substring, splitDelim) 202 | return substrings 203 | end if 204 | return {} 205 | end substringsFromString 206 | 207 | -- Merges the given list of text items using the given separator string. 208 | on mergeTextItems(textItemList, aSeparator) 209 | considering case 210 | set prevTIDs to text item delimiters of AppleScript 211 | set text item delimiters of AppleScript to aSeparator 212 | set mergedText to textItemList as text 213 | set text item delimiters of AppleScript to prevTIDs 214 | end considering 215 | return mergedText 216 | end mergeTextItems 217 | 218 | -- ------------------------------------------------------------------------------------ 219 | 220 | -- NOTE: the below handlers are written in AppleScriptObjC 221 | -- see https://latenightsw.com/adding-applescriptobjc-to-existing-scripts/ 222 | use framework "Foundation" -- required for the AppleScriptObjC handlers 223 | 224 | -- NOTE: this works around an AppleScriptObjC bug in macOS 10.13.0 225 | -- see http://latenightsw.com/high-sierra-applescriptobjc-bugs/ 226 | property NSNotFound : a reference to 9.22337203685477E+18 + 5807 227 | 228 | -- Searches the given input string using the given search pattern (which is 229 | -- treated as regular expression). Returns the substring matched by the 230 | -- entire search pattern, or an empty string (in case nothing was matched). 231 | -- @param someText The input string on which the search shall be performed 232 | -- @param searchPattern The search string as an ICU-compatible regular expression 233 | on regexMatch(someText, searchPattern) 234 | if someText is missing value then return "" 235 | if searchPattern is missing value then return "" 236 | 237 | set theString to current application's NSString's stringWithString:someText 238 | set theRange to theString's rangeOfString:searchPattern options:(current application's NSRegularExpressionSearch) 239 | if location of theRange = NSNotFound then 240 | set foundString to "" 241 | else 242 | set foundString to theString's substringWithRange:theRange 243 | end if 244 | return foundString as text 245 | end regexMatch 246 | 247 | -- Splits the given input string on the provided delimiter string. Returns the 248 | -- input string as list in case the delimiter string wasn't found in the input string. 249 | -- @param someText The input string which shall be split into substrings 250 | -- @param splitDelim The delimiter string used to split the input string 251 | on splitText(someText, splitDelim) 252 | if someText is missing value or someText is "" then return "" as list 253 | if splitDelim is missing value then return someText as list 254 | 255 | set theString to current application's NSString's stringWithString:someText 256 | set theArray to theString's componentsSeparatedByString:splitDelim 257 | return theArray as list 258 | end splitText 259 | -------------------------------------------------------------------------------- /Bookends/Update_group_publications_from_attachment_name/Update_group_publications_from_attachment_name.scpt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/extracts/mac-scripting/07649f2dadc592a68048ec88c79e31e6037eefac/Bookends/Update_group_publications_from_attachment_name/Update_group_publications_from_attachment_name.scpt -------------------------------------------------------------------------------- /DEVONthink/DEVONthink_Notes_from_PDF_Annotations/DEVONthink_Notes_from_PDF_Annotations.applescript: -------------------------------------------------------------------------------- 1 | -- DEVONthink Notes from PDF Annotations 2 | -- version 1.3, licensed under the MIT license 3 | 4 | -- by Matthias Steffens, keypoints.app, mat(at)extracts(dot)de 5 | 6 | -- For each of the PDFs selected in DEVONthink, this script will iterate over its contained PDF annotations 7 | -- and create or update a Markdown record for each markup, text or free text annotation. 8 | 9 | -- This script can also be triggered via a DEVONthink smart rule to automatically extract PDF annotations 10 | -- from imported/saved PDFs (see Setup & Usage below). 11 | 12 | -- This script requires macOS 10.14 (High Sierra) or greater, the KeypointsScriptingLib v1.5 or greater, 13 | -- and DEVONthink Pro v3.x or greater (DEVONthink Pro v3.9 or greater will be required to have deep 14 | -- links to PDF annotations work correctly). 15 | 16 | 17 | -- Setup: 18 | 19 | -- -- Before running the script, do this once: Adjust the DEVONthink label <-> color mapping via the 20 | -- properties `label1` ... `label7` below and save this script again. If saving generates an error, please 21 | -- try again with Script Debugger (which has a free "lite" mode): https://latenightsw.com/sd8/download/ 22 | 23 | -- -- You may also want to check the other properties below. These allow to customize the script, e.g. to 24 | -- enable updating of existing notes, or automatic fetching of BibTeX data. 25 | 26 | -- -- Copy the script to a suitable place, like the DEVONthink script folder. To open this folder, activate 27 | -- DEVONthink, select the Scripts menu(*) and choose "Open Scripts Folder". This will open the DEVONthink 28 | -- Scripts folder in the Finder. It is located at `~/Library/Application Scripts/com.devon-technologies.think3/Menu`. 29 | -- (*): https://download.devontechnologies.com/download/devonthink/3.8.2/DEVONthink.help/Contents/Resources/pgs/menus-scripts.html 30 | 31 | -- Alternatively, you can copy the script to the system's script menu folder. For a guide on how to enable 32 | -- and use the system's script menu, see: https://iworkautomation.com/numbers/script-menu.html 33 | 34 | -- -- If you've placed your script into the DEVONthink script folder, you may also append a keyboard shortcut 35 | -- description (like `___Command-Shift-Alt-A`) to the script's name. After restarting DEVONthink, you 36 | -- should be able to run your script via the specified keyboard shortcut. 37 | 38 | -- -- If you want this script to be triggered by a DEVONthink smart rule instead, please move the script into 39 | -- the DEVONthink smart rule scripts folder at `~/Library/Application Scripts/com.devon-technologies.think3/Smart Rules`. 40 | -- Then, in your smart rule, add an "Execute Script" action and choose "External" as well as your script 41 | -- from the dropdown menus. For more info on DEVONthink smart rules and assigning scripts, see: 42 | -- https://download.devontechnologies.com/download/devonthink/3.8.2/DEVONthink.help/Contents/Resources/pgs/automation-smartrules.html 43 | -- https://download.devontechnologies.com/download/devonthink/3.8.2/DEVONthink.help/Contents/Resources/pgs/automation-smartrulescripts.html 44 | 45 | 46 | -- Usage: 47 | 48 | -- -- Before running the script, please select one or more PDF records with PDF annotations in DEVONthink. 49 | 50 | -- -- To run the script, select its menu entry from the (DEVONthink or system's) script menu, or press your 51 | -- specified keyboard shortcut. 52 | 53 | -- -- After the script has finished, you'll see a dialog with feedback on how many PDFs have been processed 54 | -- and how many note records have been created/updated. For each PDF, its annotation notes are stored 55 | -- within a DEVONthink group next to the PDF. By default, the group is named identical to the PDF but 56 | -- contains an " – Annotations" suffix. 57 | 58 | -- -- Note that you can run the script multiple times with the same PDF record(s) selected in DEVONthink. 59 | -- On a subsequent run of the script, all notes that were newly created (or updated) will be selected. 60 | 61 | -- -- As an alternative, this script can be run automatically from within a DEVONthink smart rule that's, 62 | -- for example, triggered by an Import or Save event. Note that, when triggered by a smart rule, script 63 | -- feedback will be reported via a notification, and created/updated notes won't get selected. 64 | 65 | 66 | -- Discussion & Help: 67 | 68 | -- -- https://discourse.devontechnologies.com/t/script-to-create-individual-markdown-notes-from-pdf-annotations/80987 69 | -- https://github.com/extracts/mac-scripting/discussions 70 | 71 | 72 | -- Notes: 73 | 74 | -- -- The script will only recognize these annotation types: "Highlight", "StrikeOut", "Underline", "Squiggly", "Text" 75 | -- and "FreeText". 76 | 77 | -- -- For each selected PDF with PDF annotations, the script will create a DEVONthink group next to it containing 78 | -- Markdown record(s) for all recognized PDF annotation(s). The group's name can be controlled via the properties 79 | -- `noteFolderNamePrefix` & `noteFolderNameSuffix` below. 80 | 81 | -- -- You can run the script multiple times with the same PDF record(s) selected in DEVONthink. If PDF annotations 82 | -- have been added to the PDF file after the last script run, the next script run will add Markdown records for the 83 | -- newly created PDF annotations. Note that deletions will not get synced across. If existing PDF annotations have 84 | -- been updated the script can update the corresponding Markdown records. While this is off by default, it can be 85 | -- enabled by setting below property `updateExistingNotes` to `true`. In that case, these properties can be updated 86 | -- (if enabled below): name, note text, modification date, flagged status, rating, label, tags, custom metadata. 87 | -- Note that if you change a PDF annotation's annotation type (say, from "Highlight" to "Underline"), this will always 88 | -- create a new Markdown note. 89 | 90 | -- -- If possible, the PDF annotation's source text will get added to the body text of the Markdown record. However 91 | -- note that, depending on the tool used for PDF annotation, this may fail or be inaccurate. In that case, you may 92 | -- improve text extraction success by tweaking the properties `lineWidthEnlargement` & `lineHeightEnlargement` 93 | -- below. 94 | 95 | -- -- Notes that have been added to a PDF annotation will also get added to the Markdown record's body text. Within 96 | -- a PDF annotation note, you can use following (Keypoints-style) syntax to explicitly set the name of the Markdown 97 | -- record and/or its properties: 98 | -- - A line prefixed with `# ` (like a Markdown first-level heading) will be used as the Markdown record's name. 99 | -- - One or more line(s) prefixed with `< ` indicate a metadata line which will get stripped from the record's 100 | -- body text but which will set record properties instead: 101 | -- - In a metadata line, include 1 to 5 asterisks or `★` characters to set the record's star-rating, for example 102 | -- `< ****` would set a 4-star rating for the Markdown record. 103 | -- - In a metadata line, add the special tag `@:flagged` to set the Markdown record's flagged status to true. 104 | -- - In a metadata line, add any tags like `@tag` or `@another tag` to set these as the tags of the Markdown 105 | -- record. 106 | -- - In a metadata line, add any custom key:value attributes like `@:key:Some value` to set these as custom 107 | -- metadata of the Markdown record. A custom attribute w/o a value (like `@:key`) will get a default value 108 | -- of `true`. 109 | -- Note that you can also include any/all of these properties on a single metadata line, for example: 110 | -- `< **** @:flagged @tag @another tag @:key:Some value` 111 | 112 | -- -- If your PDF annotation notes contain custom markup syntax, you can use the `preprocessAnnotationComment()` 113 | -- method to preprocess & transform this syntax to the (Keypoints-style) format described above. 114 | 115 | -- -- For your PDF record, as well as for each of the created Markdown records, a link to the "... – Annotations" group 116 | -- folder will be copied to a custom metadata field (named `pdfannotations`). This allows you to easily get back to 117 | -- the PDF's group of annotation notes. 118 | 119 | -- -- The URL field of each Markdown record will be set to a deep link that directly points to the corresponding PDF 120 | -- annotation. I.e., clicking this deep link will open the associated PDF and scroll the corresponding PDF annotation 121 | -- into view. Note that this requires DEVONthink 3.9 or greater. 122 | 123 | -- -- For each Markdown record, the script will try to assign a color label that matches your annotation's highlight color. 124 | -- Note that, to suit your personal DEVONthink label setup, you'll need to adjust the mapping via the properties 125 | -- `label1` ... `label7` below. 126 | 127 | -- -- For each Markdown record, the script will also add an ID-like alias (like "039H-8GAB-1GPA") that's unique and 128 | -- which optionally can be used to create a stable Wiki-link (like "[[039H-8GAB-1GPA]]") to this Markdown record. 129 | 130 | -- -- For each Markdown record, the script will also add a sort identifier string (like "2-1-125" which codifies with integers 131 | -- the annotation's page, column & position from top) to the `annotationorder` custom metadata field. This metadata 132 | -- field can be used in DEVONthink to sort annotations in the order they appear in the text of a page. By default, 133 | -- sort identifiers will be generated so that they try to respect a typical 2-column text layout. Note that this may not 134 | -- always be perfect. To further control sort identifier generation, see the properties `respectMultiColumnPDFLayouts` 135 | -- and `maxTextColumns` below. 136 | 137 | -- -- If the PDF metadata contain a DOI, this DOI will get written to the `doi` custom metadata field for your PDF 138 | -- record, the "... – Annotations" group, and for each of the created Markdown records. 139 | 140 | -- -- Similarly, if the PDF record's custom metadata contain a citekey, this citekey will also get written to the `citekey` 141 | -- custom metadata field for the "... – Annotations" group, and for each of the created Markdown records. 142 | 143 | -- -- If a DOI was found for the PDF, the script will also attempt to fetch its bibliographic metadata and set the custom 144 | -- metadata and/or the Finder comment of the "... – Annotations" group & its Markdown records accordingly. Please 145 | -- see the properties below for options to disable or fine-tune this behaviour. 146 | 147 | 148 | -- Ideas for Improvement: 149 | 150 | -- allow name & content of created Markdown records to be generated via a template (e.g., to allow for custom YAML headers) 151 | -- offer a configuration option to specify which metadata shall get exported into which custom metadata field 152 | -- if just some DEVONthink groups were selected, allow to get all contained PDFs and process these 153 | -- allow to optionally look-up the PDF file in a reference manager like Bookends and auto-fetch citekey & citation info from there 154 | -- allow to (optionally) remove tags from Markdown records for PDF annotations whose corresponding tags were removed 155 | -- allow to (optionally) remove Markdown records from DEVONthink for PDF annotations that were deleted from the PDF 156 | -- support any annotation types other than "Highlight", "StrikeOut", "Underline", "Squiggly", "Text" & "FreeText" 157 | -- (see also inline TODOs in the code below) 158 | 159 | 160 | -- ------------- optionally adopt the property values below this line ------------- 161 | 162 | 163 | -- Prefix prepended to the name of the DEVONthink group that hosts a PDF file's Markdown notes. 164 | property noteFolderNamePrefix : "" 165 | 166 | -- Suffix appended to the name of the DEVONthink group that hosts a PDF file's Markdown notes. 167 | property noteFolderNameSuffix : " – Annotations" 168 | 169 | -- Maps DEVONthink label indexes to color names. Please set the `colorName` values according 170 | -- to the label <-> color mapping that you've chosen in your DEVONthink Settings under "Color". 171 | -- E.g., if your "Color" settings specify a yellowish color for the first label, then use 172 | -- `{labelIndex:1, colorName:"yellow"}`. Note that each label must have a unique color name. 173 | -- Available color names: red, orange, yellow, green, cyan, light blue, dark blue, purple, pink 174 | property label1 : {labelIndex:1, colorName:"red"} 175 | property label2 : {labelIndex:2, colorName:"green"} 176 | property label3 : {labelIndex:3, colorName:"light blue"} 177 | property label4 : {labelIndex:4, colorName:"yellow"} 178 | property label5 : {labelIndex:5, colorName:"orange"} 179 | property label6 : {labelIndex:6, colorName:"dark blue"} 180 | property label7 : {labelIndex:7, colorName:"purple"} 181 | 182 | -- Set to `true` if you want this script to update Markdown records that already exist in DEVONthink 183 | -- for PDF annotations from your PDF file(s) and which were created on previous script runs. This will 184 | -- be only necessary if you've made changes to the PDF annotations (or their associated notes) within 185 | -- the PDF file after importing them into DEVONthink. 186 | -- Notes: 187 | -- - @warning Note that updating of existing notes may override any additions/changes that you've 188 | -- made to these Markdown records in DEVONthink. 189 | -- - See below properties to specify which attributes shall get updated and which shall be left alone. 190 | property updateExistingNotes : false 191 | 192 | -- When updating existing Markdown records, specify which attributes shall get updated (`true`) 193 | -- and which shall be left alone (`false`). 194 | -- Notes: 195 | -- - Tags will only be added to any existing list of record tags, so these won't get replaced as a whole. 196 | -- Also, tags won't get removed from the existing list of record tags. 197 | -- - Custom metadata fields will also get added to any existing record metadata, and only fields 198 | -- of the same name may get replaced. 199 | property updateNameForExistingNotes : false 200 | property updateTextContentForExistingNotes : true 201 | property updateModificationDateForExistingNotes : true 202 | property updateFlaggedStatusForExistingNotes : true 203 | property updateRatingForExistingNotes : true 204 | property updateLabelForExistingNotes : true 205 | property updateTagsForExistingNotes : true 206 | property updateCustomMetadataForExistingNotes : true 207 | 208 | -- Specify if custom metadata of the DEVONthink groups that host a PDF file's Markdown notes shall 209 | -- get updated (`true`) or not (`false`). If set to `true`, this script may populate the custom metadata 210 | -- properties `sourcefile`, `citekey` & `doi`, as well as any bibliographic metadata that were fetched 211 | -- for a DOI (see below). 212 | property updateCustomMetadataForExistingFolders : true 213 | 214 | -- Set to `true` if you want this script to select all records that were actually created or updated. 215 | property selectUpdatedRecords : true 216 | 217 | -- Set to `true` if you want this script to fetch bibliographic metadata for a PDF's DOI via an online 218 | -- request to OpenAlex.org (and possibly further requests to CrossRef, see below). 219 | -- Note that setting this to `false` will disable all online requests, i.e. this will also prevent fetching 220 | -- of BibTeX data & formatted citations even if the respective properties have been set to `true` below. 221 | property fetchDOIMetadata : true 222 | 223 | -- Set to `true` if you want this script to _always_ fetch bibliographic metadata for a PDF's DOI, and 224 | -- not only once for each PDF (when creating the DEVONthink group that hosts the PDF file's Markdown 225 | -- notes). Usually, you'd want the latter (i.e. `false`). But setting this property (temporarily) to `true` 226 | -- can be useful to add or update bibliographic metadata for existing notes folders & Markdown notes. 227 | property alwaysFetchDOIMetadata : false 228 | 229 | -- When fetching bibliographic metadata for a PDF's DOI, this script can also fetch corresponding 230 | -- BibTeX data (which will be copied to a custom ("bibtex") metadata field, and which can be 231 | -- appended to the Finder comment of a created Markdown record as well (see below)). 232 | -- Set to `true` if you also want to fetch BibTeX data for the given DOI from CrossRef.org, else set 233 | -- to `false`. Note that this will cause an extra online request which may take a second or so. 234 | property fetchBibTeX : false 235 | 236 | -- Set to `true` if you want this script to append the BibTeX data (that was fetched for a PDF's DOI) 237 | -- to the Finder comment of the created Markdown record. 238 | property appendBibTeXToFinderComment : false 239 | 240 | -- When fetching bibliographic metadata for a PDF's DOI, this script can also fetch a corresponding 241 | -- formatted citation (which will be copied to a custom ("reference") metadata field, and which can 242 | -- be appended to the Finder comment of a created Markdown record as well (see below)). 243 | -- Set to `true` if you also want to fetch a formatted citation for the given DOI from CrossRef.org, 244 | -- else set to `false`. Note that this will cause an extra online request which may take a few seconds. 245 | property fetchFormattedCitation : true 246 | 247 | -- Set to `true` if you want this script to append the formatted citation (that was fetched for a PDF's 248 | -- DOI) to the Finder comment of the created Markdown record. 249 | property appendFormattedCitationToFinderComment : true 250 | 251 | -- The name of the citation style file (as obtained from https://www.zotero.org/styles) to be used 252 | -- when generating a formatted citation, e.g.: 253 | -- apa (default), apa-6th-edition, chicago-author-date, elsevier-harvard, springer-basic-author-date, 254 | -- modern-language-association, vancouver-author-date 255 | property citationStyleName : "apa" 256 | 257 | -- The locale code that specifies which localization data (term translations, localized date formats, 258 | -- and grammar options) shall be used when generating a formatted citation, e.g.: 259 | -- en-US (default), en-GB, fr-FR, es-ES, de-DE, ru-RU, zh-CN 260 | property citationLocale : "en-US" 261 | 262 | 263 | -- ----------- usually, you don't need to edit anything below this line ----------- 264 | 265 | 266 | -- Decimal numbers that specify how much the bounding box encompassing an annotation's 267 | -- individual line shall be enlarged vertically & horizontally so that it fully covers all text 268 | -- highlighted on that line. 269 | -- If you find that this script somehow fails to fully extract all of an annotation's text (or if 270 | -- it extracts too much) then you may want to adjust these decimal numbers (e.g. by 0.1 271 | -- increments). 272 | -- However, note that larger values will increase the likeliness that adjacent characters not 273 | -- belonging to the annotation will get included as well. 274 | property lineWidthEnlargement : 0.7 275 | property lineHeightEnlargement : 1.7 276 | 277 | -- Defines lower and upper hue limits for common colors. Limit values are inclusive and must be 278 | -- given as degrees (0-359) of the hue in the HSB (hue, saturation, brightness) color scheme. 279 | property redColorMapping : {colorName:"red", lowerHueLimit:350, upperHueLimit:19} 280 | property orangeColorMapping : {colorName:"orange", lowerHueLimit:20, upperHueLimit:44} 281 | property yellowColorMapping : {colorName:"yellow", lowerHueLimit:45, upperHueLimit:65} 282 | property greenColorMapping : {colorName:"green", lowerHueLimit:66, upperHueLimit:164} 283 | property cyanColorMapping : {colorName:"cyan", lowerHueLimit:165, upperHueLimit:184} 284 | property lightBlueColorMapping : {colorName:"light blue", lowerHueLimit:185, upperHueLimit:209} 285 | property darkBlueColorMapping : {colorName:"dark blue", lowerHueLimit:210, upperHueLimit:254} 286 | property purpleColorMapping : {colorName:"purple", lowerHueLimit:255, upperHueLimit:324} 287 | property pinkColorMapping : {colorName:"pink", lowerHueLimit:325, upperHueLimit:349} 288 | 289 | property colorMappings : {redColorMapping, orangeColorMapping, yellowColorMapping, greenColorMapping, cyanColorMapping, lightBlueColorMapping, darkBlueColorMapping, purpleColorMapping, pinkColorMapping} 290 | 291 | property labelMappings : {label1, label2, label3, label4, label5, label6, label7} 292 | 293 | -- Set to `true` if you want this script to associate annotations to text columns in multi-column 294 | -- PDF layouts when generating sort identifiers. 295 | -- Sort identifiers will be placed in an `annotationorder` metadata field which can be used in 296 | -- DEVONthink for sorting so that annotations can be listed in the order they appear in the text 297 | -- of a page (optionally respecting a multi-column layout). 298 | -- Sort identifier format: `--` (e.g. "2-1-207"). 299 | -- If this property is set to `false`, `` will be always "1". 300 | -- Note that, depending on the layout of the PDF page and the specific annotation's width, 301 | -- correctly guessing the annotation's text column may still fail. 302 | property respectMultiColumnPDFLayouts : true 303 | 304 | -- The number of text columns supported by this script when generating sort identifiers. 305 | -- If you're often dealing with PDF text layouts that have more than the specified number of text 306 | -- columns then you may want to adjust this integer number. However, note that larger values 307 | -- will increase the likeliness that short annotations or annotations from document parts 308 | -- spanning multiple columns (like the abstract) won't sort correctly. 309 | property maxTextColumns : 2 310 | 311 | -- Approximate average width of the (left or right) white space between text & page origin/end. 312 | -- Note that this is just a wild guess for the average margin of a PDF page as properly calculating 313 | -- margins isn't straightforward. However, specifying some value for an average margin usually 314 | -- helps when trying to associate annotations to text columns in multi-column PDF layouts. 315 | property pageMargin : 20 316 | 317 | property createdNotesCount : 0 318 | property updatedNotesCount : 0 319 | property pdfCount : 0 320 | 321 | -- The "KeypointsScriptingLib.scptd" scripting library provides utility methods for this script. 322 | -- It can be made available to this script by copying it into a "Script Libraries" folder inside 323 | -- the "Library" folder that's within your Home folder. 324 | -- see https://github.com/extracts/mac-scripting/tree/master/ScriptingLibraries/KeypointsScriptingLib 325 | use KeypointsLib : script "KeypointsScriptingLib" -- v1.5 or greater required 326 | 327 | -- NOTE: In order to allow this script to be executed by a DEVONthink smart rule (which requires 328 | -- pure AppleScript code), all AppleScriptObjC code has to be run from the included scripting library, 329 | -- and use statements such as `use framework "Foundation"` and `use scripting additions` must 330 | -- not be used. For a workaround to this, see: 331 | -- https://discourse.devontechnologies.com/t/solution-to-the-problem-of-using-the-applescript-foundation-framework-in-smart-rules/78575 332 | 333 | 334 | -- This method gets triggered when executing this script via a DEVONthink smart rule. 335 | -- @param theRecords List of records as defined by the calling smart rule. 336 | on performSmartRule(theRecords) 337 | set annotatedPDFs to my onlyDEVONthinkPDFsWithPDFAnnotations(theRecords) 338 | 339 | if annotatedPDFs is not {} then 340 | set updatedRecords to my processPDFs(annotatedPDFs) 341 | end if 342 | 343 | -- display feedback (number of PDFs processed and notes created/updated) as a notification 344 | set completionDetails to "Processed PDFs: " & pdfCount & linefeed & "Created notes: " & createdNotesCount & " | " & "Updated notes: " & updatedNotesCount 345 | KeypointsLib's displayNotification(completionDetails, "Finished Import of PDF Annotations", "") 346 | end performSmartRule 347 | 348 | 349 | -- This method gets triggered when executing this script manually. It will process all PDFs 350 | -- that are currently selected in DEVONthink. If there's currently no selection in DEVONthink 351 | -- (or if the selection contains no PDFs with PDF annotations) presents an error alert and 352 | -- aborts the script. 353 | on run 354 | -- DEVONthink must be running for this script to work 355 | if not my checkAppsRunning() then return 356 | 357 | -- only deal with currently selected PDFs that contain PDF annotations 358 | tell application id "DNtp" 359 | set selRecords to (selection as list) 360 | set annotatedPDFs to my onlyDEVONthinkPDFsWithPDFAnnotations(selRecords) 361 | 362 | if annotatedPDFs is {} then KeypointsLib's displayError("No PDF(s) with PDF annotations selected!", "Please open DEVONthink and select some PDF(s) with PDF annotations.", 15, true) 363 | end tell 364 | 365 | set updatedRecords to my processPDFs(annotatedPDFs) 366 | 367 | -- select records that were created or updated 368 | if selectUpdatedRecords is true and updatedRecords is not {} then 369 | tell application id "DNtp" 370 | set frontWindow to viewer window 1 371 | set selection of frontWindow to updatedRecords 372 | end tell 373 | end if 374 | 375 | -- display a dialog with feedback (number of PDFs processed and notes created/updated) 376 | tell application id "DNtp" 377 | activate 378 | set completionDetails to "Processed PDFs: " & pdfCount & linefeed & "Created notes: " & createdNotesCount & linefeed & "Updated notes: " & updatedNotesCount 379 | KeypointsLib's displayMessage("Finished Import of PDF Annotations", completionDetails, false, 0) 380 | end tell 381 | end run 382 | 383 | 384 | -- Main method which iterates over the given PDF records and processes any contained 385 | -- PDF annotations. 386 | -- Returns a list with all records that were actually updated. I.e., if a PDF annotation did 387 | -- already have a corresponding Markdown record in DEVONthink which wasn't updated 388 | -- (since its text content & properties were already up-to-date), it won't get included in 389 | -- the returned list. 390 | -- @param pdfRecords The DEVONthink PDF records that shall be processed. 391 | on processPDFs(pdfRecords) 392 | KeypointsLib's setupProgress("Creating Markdown notes for PDF annotations") 393 | 394 | -- initialize progress reporting 395 | set createdNotesCount to 0 396 | set updatedNotesCount to 0 397 | set pdfCount to count of pdfRecords 398 | KeypointsLib's setTotalStepsForProgress(pdfCount) 399 | 400 | -- process PDF annotations for each PDF 401 | set allUpdatedRecords to {} 402 | repeat with i from 1 to pdfCount 403 | set pdfRecord to item i of pdfRecords 404 | tell application id "DNtp" to set pdfFilename to filename of pdfRecord 405 | KeypointsLib's updateProgress(i, "Processing PDF " & i & " of " & pdfCount & " (\"" & pdfFilename & "\").") 406 | set updatedRecords to my createDEVONthinkNotesForPDF(pdfRecord) 407 | if updatedRecords is not {} then copy updatedRecords to end of allUpdatedRecords 408 | end repeat 409 | 410 | return KeypointsLib's flattenList(allUpdatedRecords) 411 | end processPDFs 412 | 413 | 414 | -- Checks if DEVONthink Pro is running. 415 | -- Credits: modified after script code by Rob Trew 416 | -- see https://github.com/RobTrew/tree-tools/blob/master/DevonThink%20scripts/Sente6ToDevn73.applescript 417 | on checkAppsRunning() 418 | tell application id "sevs" -- application "System Events" 419 | if (count of (processes where creator type = "DNtp")) < 1 then 420 | KeypointsLib's displayError("DEVONthink Pro not running!", "Please open DEVONthink Pro and select some PDF(s), then run this script again.", 15, true) 421 | return false 422 | end if 423 | end tell 424 | return true 425 | end checkAppsRunning 426 | 427 | 428 | -- Returns all PDF records from the given DEVONthink records that contain some 429 | -- PDF annotations. 430 | -- @param theRecords The DEVONthink records that shall be processed. 431 | on onlyDEVONthinkPDFsWithPDFAnnotations(theRecords) 432 | set annotatedPDFs to {} 433 | 434 | tell application id "DNtp" 435 | repeat with theRecord in theRecords 436 | if (type of theRecord is PDF document) and (annotation count of theRecord > 0) then 437 | copy theRecord to end of annotatedPDFs 438 | end if 439 | end repeat 440 | end tell 441 | 442 | return annotatedPDFs 443 | end onlyDEVONthinkPDFsWithPDFAnnotations 444 | 445 | 446 | -- Iterates over the given PDF's contained PDF annotations and creates a DEVONthink 447 | -- record for each markup, text or free text annotation (if it doesn't exist yet). 448 | -- Returns a list with all records that were actually updated. I.e., if a PDF annotation did 449 | -- already have a corresponding Markdown record in DEVONthink which wasn't updated 450 | -- (since its text content & properties were already up-to-date), it won't get included in 451 | -- the returned list. 452 | -- @param pdfRecord The DEVONthink record representing the PDF whose annotations 453 | -- shall be extracted. 454 | on createDEVONthinkNotesForPDF(pdfRecord) 455 | -- assemble info for the PDF record and its (possibly to be created) notes folder 456 | tell application id "DNtp" 457 | set pdfPath to path of pdfRecord 458 | set pdfurl to reference URL of pdfRecord 459 | set pdfLocationPath to location of pdfRecord 460 | 461 | set pdfMetadata to custom meta data of pdfRecord 462 | try 463 | -- pdfMetadata may be undefined in which case this triggers the error case 464 | if pdfMetadata is missing value then set pdfMetadata to {} 465 | on error 466 | set pdfMetadata to {} 467 | end try 468 | 469 | -- if necessary, create the notes folder, i.e. the DEVONthink group that hosts the PDF file's Markdown notes 470 | set noteFolderName to noteFolderNamePrefix & name of pdfRecord & noteFolderNameSuffix 471 | set folderLocationPath to (pdfLocationPath & noteFolderName & "/") 472 | set folderDidExist to exists record at folderLocationPath 473 | set folderLocation to create location folderLocationPath 474 | set folderURL to reference URL of folderLocation 475 | set URL of folderLocation to pdfurl & "?page=0" 476 | 477 | set folderMetadata to custom meta data of folderLocation 478 | try 479 | -- folderMetadata may be undefined in which case this triggers the error case 480 | if folderMetadata is missing value then set folderMetadata to {} 481 | on error 482 | set folderMetadata to {} 483 | end try 484 | end tell 485 | 486 | -- extract any DOI & citekey for the PDF from its custom metadata 487 | set sourceDOI to "" 488 | set sourceCitekey to "" 489 | if pdfMetadata is not {} then 490 | set metadataDOI to (KeypointsLib's valueForKey:"mddoi" inRecord:pdfMetadata) 491 | if metadataDOI is not missing value and metadataDOI is not "" then 492 | set sourceDOI to KeypointsLib's matchDOI(metadataDOI) 493 | end if 494 | set metadataCitekey to (KeypointsLib's valueForKey:"mdcitekey" inRecord:pdfMetadata) 495 | if metadataCitekey is not missing value and metadataCitekey is not "" then 496 | set sourceCitekey to metadataCitekey 497 | end if 498 | end if 499 | 500 | -- set KeypointsLib's properties which control some of its annotation-related methods 501 | set KeypointsLib's lineWidthEnlargement to lineWidthEnlargement 502 | set KeypointsLib's lineHeightEnlargement to lineHeightEnlargement 503 | set KeypointsLib's redColorMapping to redColorMapping -- KeypointsLib's other individual color mappings don't need to be set explicitly 504 | set KeypointsLib's colorMappings to colorMappings 505 | set KeypointsLib's respectMultiColumnPDFLayouts to respectMultiColumnPDFLayouts 506 | set KeypointsLib's maxTextColumns to maxTextColumns 507 | set KeypointsLib's pageMargin to pageMargin 508 | 509 | -- assemble info for all PDF annotations as a list of property records 510 | set {pdfAnnotationsList, sourceDOI} to KeypointsLib's pdfAnnotationInfo(pdfPath, pdfurl, sourceDOI, sourceCitekey) 511 | 512 | -- sort all PDF annotations so that they are listed in the order they appear in the document & on the page 513 | -- NOTE: by default, PDF annotations seem to be ordered by page & creation date (?) 514 | --set pdfAnnotationsList to KeypointsLib's sortList:pdfAnnotationsList byKey:"sortString" inAscendingOrder:true usingSelector:"localizedStandardCompare:" 515 | 516 | -- set custom metadata fields for the PDF record: DOI & DT link back to the notes folder 517 | set pdfMetadata to pdfMetadata & {doi:sourceDOI, pdfannotations:folderURL} 518 | my setMetadataForDTRecord(pdfRecord, pdfMetadata) 519 | 520 | -- if the notes folder just got created, set its metadata 521 | set bibMetadata to {} 522 | set formattedCitation to "" 523 | set bibTeXData to "" 524 | if folderDidExist is false or alwaysFetchDOIMetadata is true then 525 | -- fetch bibliographic metadata for the PDF's DOI 526 | if fetchDOIMetadata is true and sourceDOI is not missing value and sourceDOI is not "" then 527 | set bibMetadata to my bibMetadataForDOI(sourceDOI, sourceCitekey) 528 | if bibMetadata is not {} then 529 | set formattedCitation to (KeypointsLib's valueForKey:"reference" inRecord:bibMetadata) 530 | set bibTeXData to (KeypointsLib's valueForKey:"bibtex" inRecord:bibMetadata) 531 | end if 532 | end if 533 | 534 | set folderMetadata to folderMetadata & {sourceFile:pdfPath, citekey:sourceCitekey, doi:sourceDOI} 535 | set folderMetadata to folderMetadata & bibMetadata 536 | 537 | my setMetadataForDTFolder(folderLocation, folderMetadata) 538 | end if 539 | 540 | -- if the notes folder just got created, append BibTeX data and/or the formatted citation to its Finder comment 541 | if folderDidExist is false then 542 | if appendFormattedCitationToFinderComment is true and formattedCitation is not missing value and formattedCitation is not "" then 543 | my appendToCommentOfDTFolder(folderLocation, formattedCitation) 544 | end if 545 | if appendBibTeXToFinderComment is true and bibTeXData is not missing value and bibTeXData is not "" then 546 | my appendToCommentOfDTFolder(folderLocation, bibTeXData) 547 | end if 548 | end if 549 | 550 | set updatedRecords to {} 551 | 552 | -- loop over each markup, text or free text annotation in the PDF and create/update its corresponding Markdown record 553 | repeat with pdfAnnotation in pdfAnnotationsList 554 | set aComment to (pdfAnnotation's comment) 555 | if aComment is not missing value then 556 | set aComment to my preprocessAnnotationComment(aComment as string) 557 | end if 558 | 559 | set annotText to (pdfAnnotation's annotText) 560 | if annotText is not missing value then set annotText to annotText as string 561 | 562 | set aPageLabel to (pdfAnnotation's pageLabel) 563 | 564 | -- assemble record content from annotation text & comment 565 | set recordContents to my recordContentFromPDFAnnotationData(annotText, aComment) 566 | 567 | -- assemble record name from annotation text, comment & page label 568 | set recordName to my recordNameFromPDFAnnotationData(annotText, aComment, aPageLabel) 569 | 570 | -- to ensure stable Keypoints IDs, the last part of the annotation's sort identifier string (like "2-1-125"), 571 | -- i.e. its position from top, will be used to form the fixed "milliseconds" part when creating a Keypoints ID 572 | set annotSortString to (pdfAnnotation's sortString) as string 573 | set positionFromTop to (pdfAnnotation's positionFromTop) as string 574 | set centiSeconds to text -2 thru -1 of ("0" & positionFromTop) 575 | 576 | -- record modification & creation date 577 | set recordCreationDate to (pdfAnnotation's createdDate) as date 578 | set recordModificationDate to (pdfAnnotation's modifiedDate) as date 579 | if recordModificationDate is not missing value then 580 | set recordAliases to KeypointsLib's keypointsIDForDate(recordCreationDate, centiSeconds) 581 | end if 582 | 583 | set recordURL to (pdfAnnotation's deepLink) as string 584 | 585 | -- record metadata 586 | set annotType to (pdfAnnotation's annotType) as string 587 | set recordMetadata to bibMetadata & {pdfannotations:folderURL, annotationType:annotType, annotationOrder:annotSortString} 588 | 589 | set aUserName to (pdfAnnotation's userName) 590 | if aUserName is not missing value and aUserName is not "" then set recordMetadata to recordMetadata & {createdBy:(aUserName as string)} 591 | 592 | if aPageLabel is not missing value and aPageLabel is not "" then set recordMetadata to recordMetadata & {sourcePage:(aPageLabel as string)} 593 | 594 | if pdfPath is not missing value then set recordMetadata to recordMetadata & {sourceFile:pdfPath} 595 | 596 | -- TODO: allow to extract (& prioritize) the citekey from the annotation comment (e.g. `< #Miller+Johns2024` or `< [#Miller+Johns2024]`) 597 | set citekey to (pdfAnnotation's citekey) 598 | if citekey is not missing value and citekey is not "" then set recordMetadata to recordMetadata & {citekey:citekey} 599 | 600 | if sourceDOI is not missing value and sourceDOI is not "" then set recordMetadata to recordMetadata & {doi:sourceDOI} 601 | 602 | -- extract tags (like `< @tag @another tag` or `< [@tag] [@another tag]`) as well as custom attributes 603 | -- (i.e., special tags like `< @:key:value @:key` or `< [@:key:value] [@:key]`) within the annotation comment 604 | set {customAttributes, recordTags} to KeypointsLib's customAttributesAndTagsFromKeypointsNote(aComment, {"flagged"}) 605 | 606 | -- set the extracted custom attributes as dedicated record metadata 607 | repeat with customAttribute in customAttributes 608 | set aKey to customAttribute's attribKey 609 | set aValue to customAttribute's attribValue 610 | 611 | -- allow multiple occurrences of the same custom key with different values (e.g. `@:key:Some value @:key:Other value`) 612 | set existingValue to (KeypointsLib's valueForKey:aKey inRecord:recordMetadata) 613 | if existingValue is not missing value then 614 | set recordMetadata to (KeypointsLib's setValue:(existingValue & ";" & aValue) forKey:aKey inRecord:recordMetadata) 615 | else 616 | set recordMetadata to recordMetadata & (KeypointsLib's recordFromLabels:{aKey} andValues:{aValue}) 617 | end if 618 | end repeat 619 | 620 | -- honor a flagged status (like `< @:flagged`) within the annotation comment 621 | set isFlagged to KeypointsLib's keypointsNoteIsMarkedAsFlagged(aComment) 622 | 623 | -- honor a rating (like `< ***`) within the annotation comment 624 | set recordRating to KeypointsLib's keypointsNoteRatingNumber(aComment) 625 | 626 | -- attempt to map the annotation's color to a DEVONthink label index 627 | -- TODO: allow to extract (& prioritize) the DEVONthink label from the annotation comment (e.g. `< (@Important)` or `< @1`) 628 | set recordLabelIndex to my labelIndexForColorName(pdfAnnotation's annotColorName) 629 | 630 | -- create a Markdown record for this annotation in DEVONthink 631 | set {dtRecord, recordWasUpdated} to my createDTRecord(folderLocation, folderMetadata, recordName, recordAliases, recordURL, recordTags, isFlagged, recordContents, recordCreationDate, recordModificationDate, recordMetadata, recordRating, recordLabelIndex) 632 | 633 | if recordWasUpdated is true then 634 | copy dtRecord to end of updatedRecords 635 | end if 636 | end repeat 637 | 638 | return updatedRecords 639 | end createDEVONthinkNotesForPDF 640 | 641 | 642 | -- Assembles the content of a Markdown record from the given PDF annotation text 643 | -- and comment. 644 | on recordContentFromPDFAnnotationData(annotText, annotComment) 645 | set recordContentParts to {} 646 | if annotText is not missing value and annotText is not "" then 647 | copy "> " & (annotText as string) to end of recordContentParts 648 | end if 649 | 650 | if annotComment is not missing value and annotComment is not "" then 651 | set processedComment to annotComment 652 | 653 | -- ensure that a first-level heading at the top of the annotation comment stays there 654 | -- (even when annotation text gets inserted as a quotation) 655 | set firstLevelHeadingRegex to "^(?:\\s*[\\r\\n])*(#[ \\t]+.+)" 656 | set firstLevelHeading to KeypointsLib's regexMatch(annotComment, firstLevelHeadingRegex) 657 | if firstLevelHeading is not "" then 658 | set firstLevelHeading to KeypointsLib's regexReplace(firstLevelHeading, firstLevelHeadingRegex, "$1") 659 | copy firstLevelHeading to beginning of recordContentParts 660 | 661 | set processedComment to KeypointsLib's regexReplace(processedComment, firstLevelHeadingRegex & "[\\r\\n]?(\\s*[\\r\\n])*", "") 662 | end if 663 | 664 | -- for the record content, add the remaining annotation comment w/o any metadata lines (which start with "< ") 665 | set processedComment to KeypointsLib's keypointsNoteWithoutMetadataLines(processedComment, false) 666 | 667 | copy processedComment to end of recordContentParts 668 | end if 669 | 670 | if recordContentParts is not {} then 671 | set recordContents to KeypointsLib's mergeTextItems(recordContentParts, linefeed & linefeed) 672 | else 673 | set recordContents to "(no content)" 674 | end if 675 | 676 | return recordContents 677 | end recordContentFromPDFAnnotationData 678 | 679 | 680 | -- Assembles the name of a Markdown record from the given PDF annotation text, 681 | -- comment and page label. 682 | on recordNameFromPDFAnnotationData(annotText, annotComment, pageLabel) 683 | set recordNameParts to {} 684 | if pageLabel is not missing value and pageLabel is not "" then 685 | copy "p" & (pageLabel as string) to end of recordNameParts 686 | end if 687 | 688 | set recordNamePart to "" 689 | if annotComment is not missing value and annotComment is not "" then 690 | -- for the record's name, use any first-level heading (if there's one in the annotation's comment) 691 | set markdownHeadings to KeypointsLib's markdownHeadingsFromText(annotComment, "#") 692 | if markdownHeadings is not {} then 693 | set recordNamePart to heading of first item of markdownHeadings 694 | end if 695 | 696 | -- otherwise, use up to 12 words from the beginning of the "comment" 697 | if recordNamePart is "" then 698 | set annotComment to KeypointsLib's keypointsNoteWithoutMetadataLines(annotComment, true) 699 | set recordNamePart to KeypointsLib's firstWordsFromText(annotComment, 12) 700 | end if 701 | end if 702 | 703 | -- else use up to 12 words from the beginning of the "annotText" 704 | if recordNamePart is "" then 705 | set recordNamePart to KeypointsLib's firstWordsFromText(annotText, 12) 706 | if recordNamePart is not "" then 707 | -- wrap the extracted text in quotes to indicate that it's quoted text (and not your own comment) 708 | set recordNamePart to "\"" & recordNamePart & "\"" 709 | end if 710 | end if 711 | 712 | if recordNamePart is "" then set recordNamePart to "(untitled)" 713 | copy recordNamePart to end of recordNameParts 714 | 715 | if recordNameParts is not {} then 716 | set recordName to KeypointsLib's mergeTextItems(recordNameParts, ": ") 717 | else 718 | set recordName to "(untitled)" 719 | end if 720 | 721 | return recordName 722 | end recordNameFromPDFAnnotationData 723 | 724 | 725 | -- Creates a new (Markdown) record in DEVONthink with the given text & properties 726 | -- and returns it. If an existing record with the same URL exists at the same location, 727 | -- this record will get updated and returned instead. 728 | -- Note that the actual return value is a list with two items with the first item being the 729 | -- created/modified record and the second item being a boolean value indicating if the 730 | -- returned record actually has been modified ('true') or not (`false`): 731 | -- `{dtRecord, didUpdateNote}` 732 | -- Credits: modified after script code by Rob Trew 733 | -- see https://github.com/RobTrew/tree-tools/blob/master/DevonThink%20scripts/Sente6ToDevn73.applescript 734 | on createDTRecord(folderLocation, folderMetadata, recordName, recordAliases, recordURL, recordTags, isFlagged, recordText, recordCreationDate, recordModificationDate, recordMetadata, recordRating, recordLabelIndex) 735 | tell application id "DNtp" 736 | set newRecordData to {type:markdown, content:recordText, name:recordName} 737 | 738 | if recordCreationDate is not missing value then 739 | set newRecordData to newRecordData & {creation date:recordCreationDate} 740 | end if 741 | 742 | if recordURL is not "" then 743 | set newRecordData to newRecordData & {URL:recordURL} 744 | end if 745 | 746 | if recordAliases is not "" then 747 | set newRecordData to newRecordData & {aliases:recordAliases} 748 | end if 749 | 750 | set aRecord to missing value 751 | set didCreateNote to false 752 | set didUpdateNote to false 753 | 754 | -- use any existing record with the same URL that exists at the same location 755 | set isExistingRecord to false 756 | if recordURL is not "" then 757 | set existingRecords to lookup records with URL recordURL 758 | if existingRecords is not {} then 759 | set targetLocation to location of folderLocation & name of folderLocation & "/" 760 | repeat with existingRecord in existingRecords 761 | if (aRecord is missing value) and (location of existingRecord = targetLocation) then 762 | set aRecord to existingRecord 763 | set isExistingRecord to true 764 | end if 765 | end repeat 766 | end if 767 | end if 768 | 769 | if aRecord is missing value then 770 | set aRecord to create record with newRecordData in folderLocation 771 | 772 | -- newly created Markdown notes inherit their notes folder's existing metadata & Finder comment 773 | set recordMetadata to recordMetadata & folderMetadata 774 | set aRecord's comment to folderLocation's comment 775 | 776 | set didCreateNote to true 777 | set createdNotesCount to createdNotesCount + 1 778 | end if 779 | 780 | if isExistingRecord is false or updateExistingNotes is true then 781 | if updateNameForExistingNotes is true then 782 | if name of aRecord ≠ recordName then 783 | set didUpdateNote to true 784 | set name of aRecord to recordName 785 | end if 786 | end if 787 | 788 | if updateTextContentForExistingNotes is true then 789 | if plain text of aRecord ≠ recordText then 790 | set didUpdateNote to true 791 | set plain text of aRecord to recordText 792 | end if 793 | end if 794 | 795 | if (isExistingRecord is false or updateModificationDateForExistingNotes is true) and recordModificationDate is not missing value then 796 | -- TODO: should we force update recordModificationDate when an existing record's property got updated? 797 | if modification date of aRecord ≠ recordModificationDate then 798 | set didUpdateNote to true 799 | set modification date of aRecord to recordModificationDate 800 | end if 801 | end if 802 | 803 | if isExistingRecord is false or updateFlaggedStatusForExistingNotes is true then 804 | if state of aRecord ≠ isFlagged then 805 | set didUpdateNote to true 806 | set state of aRecord to isFlagged 807 | end if 808 | end if 809 | 810 | if isExistingRecord is false or updateRatingForExistingNotes is true then 811 | if rating of aRecord ≠ recordRating then 812 | set didUpdateNote to true 813 | set rating of aRecord to recordRating 814 | end if 815 | end if 816 | 817 | if isExistingRecord is false or updateLabelForExistingNotes is true then 818 | if label of aRecord ≠ recordLabelIndex then 819 | set didUpdateNote to true 820 | set label of aRecord to recordLabelIndex 821 | end if 822 | end if 823 | 824 | if (isExistingRecord is false or updateTagsForExistingNotes is true) and recordTags is not {} then 825 | set existingRecordTags to tags of aRecord 826 | repeat with aTag in recordTags 827 | if aTag is not in existingRecordTags then set didUpdateNote to true 828 | end repeat 829 | set tags of aRecord to existingRecordTags & recordTags 830 | end if 831 | 832 | if (isExistingRecord is false or updateCustomMetadataForExistingNotes is true) then 833 | if (my updateMetadataForDTRecord(aRecord, recordMetadata)) then 834 | set didUpdateNote to true 835 | end if 836 | end if 837 | 838 | if didUpdateNote is true and didCreateNote is false then 839 | set updatedNotesCount to updatedNotesCount + 1 840 | end if 841 | end if 842 | 843 | return {aRecord, didUpdateNote} 844 | end tell 845 | end createDTRecord 846 | 847 | 848 | -- Sets the custom metadata of the given DEVONthink group. 849 | -- @param folderLocation The DEVONthink group whose custom metadata shall be set. 850 | -- @param folderMetadata The record of custom metadata that shall be set. 851 | on setMetadataForDTFolder(folderLocation, folderMetadata) 852 | if updateCustomMetadataForExistingFolders is true and folderLocation is not missing value and folderMetadata is not missing value and folderMetadata is not {} then 853 | tell application id "DNtp" to set custom meta data of folderLocation to folderMetadata 854 | end if 855 | end setMetadataForDTFolder 856 | 857 | 858 | -- Sets the custom metadata of the given DEVONthink record. 859 | -- @param aRecord The DEVONthink record whose custom metadata shall be set. 860 | -- @param recordMetadata The record of custom metadata that shall be set. 861 | on setMetadataForDTRecord(aRecord, recordMetadata) 862 | if aRecord is not missing value and recordMetadata is not missing value and recordMetadata is not {} then 863 | tell application id "DNtp" to set custom meta data of aRecord to recordMetadata 864 | end if 865 | end setMetadataForDTRecord 866 | 867 | 868 | -- Updates the custom metadata of the given DEVONthink record. Returns a boolean value indicating if the 869 | -- given record actually has been modified ('true') or not (`false`). 870 | -- @param aRecord The DEVONthink record whose custom metadata shall be updated. 871 | -- @param recordMetadata The record of custom metadata that shall be set. 872 | on updateMetadataForDTRecord(aRecord, recordMetadata) 873 | if aRecord is missing value or recordMetadata is missing value or recordMetadata is {} then return false 874 | 875 | set didUpdateNote to false 876 | 877 | tell application id "DNtp" 878 | set existingRecordMetadata to custom meta data of aRecord 879 | try 880 | -- existingRecordMetadata may be undefined in which case this triggers the error case 881 | if existingRecordMetadata is missing value then set existingRecordMetadata to {} 882 | on error 883 | set existingRecordMetadata to {} 884 | end try 885 | 886 | if existingRecordMetadata is not {} then 887 | set existingKeys to KeypointsLib's keysOfRecord:existingRecordMetadata 888 | set theKeys to KeypointsLib's keysOfRecord:recordMetadata 889 | 890 | -- NOTE: updating non-empty values for existing keys doesn't seem to work unless we clear the key's value or remove the key entirely 891 | repeat with theKey in theKeys 892 | set dtKey to my dtCustomMetadataIdentifierForName(theKey) 893 | 894 | if dtKey is in existingKeys then 895 | set theValue to (KeypointsLib's valueForKey:theKey inRecord:recordMetadata) 896 | set existingValue to (KeypointsLib's valueForKey:dtKey inRecord:existingRecordMetadata) 897 | 898 | if theValue ≠ existingValue then 899 | set didUpdateNote to true 900 | set existingRecordMetadata to (KeypointsLib's setValue:(missing value) forKey:dtKey inRecord:existingRecordMetadata) 901 | end if 902 | else 903 | set didUpdateNote to true 904 | end if 905 | end repeat 906 | 907 | set recordMetadata to KeypointsLib's addItemsFromRecord:recordMetadata toRecord:existingRecordMetadata 908 | end if 909 | 910 | set custom meta data of aRecord to recordMetadata 911 | end tell 912 | 913 | return didUpdateNote 914 | end updateMetadataForDTRecord 915 | 916 | 917 | -- Returns the metadata identifier name that DEVONthink would use in scripting contexts 918 | -- for the given key name. 919 | on dtCustomMetadataIdentifierForName(theKey) 920 | set dtKey to KeypointsLib's lowercaseText(theKey) 921 | set dtKey to KeypointsLib's regexReplace(dtKey, "\\s+", "") 922 | 923 | return "md" & dtKey 924 | end dtCustomMetadataIdentifierForName 925 | 926 | 927 | -- Appends the given string to the Finder comment of the given DEVONthink group. 928 | on appendToCommentOfDTFolder(folderLocation, folderComment) 929 | if folderComment is not missing value and folderComment is not {} then 930 | set commentParts to {} 931 | tell application id "DNtp" 932 | set existingFolderComment to (comment of folderLocation) 933 | if existingFolderComment is not missing value and existingFolderComment is not "" then copy existingFolderComment to end of commentParts 934 | copy folderComment to end of commentParts 935 | 936 | set comment of folderLocation to KeypointsLib's mergeTextItems(commentParts, linefeed & linefeed) 937 | end tell 938 | end if 939 | end appendToCommentOfDTFolder 940 | 941 | 942 | -- Returns a record of bibliographic metadata for the given DOI (as fetched from OpenAlex.org), 943 | -- ready to be used as custom metadata in DEVONthink. 944 | -- @param doi The DOI for which bibliographic metadata shall be fetched. 945 | -- @param citekey The citekey to be used with the fetched BibTeX data; may be empty in which 946 | -- case a default citekey will be used. 947 | -- TODO: allow to specify which metadata shall get exported into which custom metadata field 948 | on bibMetadataForDOI(doi, citekey) 949 | if doi is missing value or doi is "" then return {} 950 | 951 | set publicationData to KeypointsLib's metadataForDOI(doi, fetchBibTeX, fetchFormattedCitation, "apa", "en-GB", citekey) 952 | if publicationData is {} then return {} 953 | 954 | set bibMetadata to {} 955 | 956 | set pubAuthors to KeypointsLib's valueForKey:"authors" inRecord:publicationData -- single author: string, multiple authors: list 957 | if pubAuthors is not missing value and pubAuthors is not "" and pubAuthors is not {} then 958 | set bibMetadata to bibMetadata & {authors:(KeypointsLib's mergeTextItems(pubAuthors, linefeed))} 959 | 960 | set pubAuthorsCount to count of pubAuthors 961 | if pubAuthorsCount is 1 then 962 | set bibMetadata to bibMetadata & {author:first item of pubAuthors} 963 | else if pubAuthorsCount is 2 then 964 | set bibMetadata to bibMetadata & {author:first item of pubAuthors & " & " & second item of pubAuthors} 965 | else -- 3 or more 966 | set bibMetadata to bibMetadata & {author:first item of pubAuthors & " et al."} 967 | end if 968 | end if 969 | 970 | set pubDate to KeypointsLib's valueForKey:"date" inRecord:publicationData 971 | if pubDate is not missing value then set bibMetadata to bibMetadata & {|date|:pubDate as date} 972 | 973 | set pubPublisher to KeypointsLib's valueForKey:"publisher" inRecord:publicationData 974 | if pubPublisher is not missing value and pubPublisher is not "" then set bibMetadata to bibMetadata & {publisher:pubPublisher} 975 | 976 | set pubISSN to KeypointsLib's valueForKey:"issn" inRecord:publicationData 977 | if pubISSN is not missing value and pubISSN is not "" then set bibMetadata to bibMetadata & {|is?n|:pubISSN} 978 | 979 | set pubJournal to KeypointsLib's valueForKey:"journal" inRecord:publicationData 980 | if pubJournal is not missing value and pubJournal is not "" then set bibMetadata to bibMetadata & {journal:pubJournal} 981 | 982 | set pubVolume to KeypointsLib's valueForKey:"volume" inRecord:publicationData 983 | set pubIssue to KeypointsLib's valueForKey:"issue" inRecord:publicationData 984 | if pubVolume is not missing value and pubVolume is not "" then 985 | if pubIssue is not missing value and pubIssue is not "" then set pubVolume to pubVolume & "(" & pubIssue & ")" 986 | set bibMetadata to bibMetadata & {volume:pubVolume} 987 | end if 988 | 989 | set pubPages to KeypointsLib's valueForKey:"page" inRecord:publicationData 990 | if pubPages is not missing value and pubPages is not "" then set bibMetadata to bibMetadata & {page:pubPages} 991 | 992 | set pubCitation to KeypointsLib's valueForKey:"citation" inRecord:publicationData 993 | if pubCitation is not missing value and pubCitation is not "" then set bibMetadata to bibMetadata & {|reference|:pubCitation} 994 | 995 | set pubBibTeX to KeypointsLib's valueForKey:"bibtex" inRecord:publicationData 996 | if pubBibTeX is not missing value and pubBibTeX is not "" then set bibMetadata to bibMetadata & {bibtex:pubBibTeX} 997 | 998 | set pubLink to KeypointsLib's valueForKey:"url" inRecord:publicationData 999 | if pubLink is not missing value and pubLink is not "" then set bibMetadata to bibMetadata & {link:pubLink} 1000 | 1001 | set pubPMID to KeypointsLib's valueForKey:"pmid" inRecord:publicationData 1002 | if pubPMID is not missing value and pubPMID is not "" then set bibMetadata to bibMetadata & {pmid:pubPMID} 1003 | 1004 | set pubPMCID to KeypointsLib's valueForKey:"pmcid" inRecord:publicationData 1005 | if pubPMCID is not missing value and pubPMCID is not "" then set bibMetadata to bibMetadata & {pmcid:pubPMCID} 1006 | 1007 | return bibMetadata 1008 | end bibMetadataForDOI 1009 | 1010 | 1011 | -- Maps the given color name to a DEVONthink label index and returns it. 1012 | on labelIndexForColorName(colorName) 1013 | if colorName is missing value or colorName is "" then return 0 1014 | 1015 | set recordLabelIndex to 0 1016 | set labelMapping to (KeypointsLib's recordForKey:"colorName" andValue:colorName inListOfRecords:labelMappings) 1017 | if labelMapping is not missing value then 1018 | set recordLabelIndex to labelMapping's labelIndex 1019 | end if 1020 | 1021 | return recordLabelIndex 1022 | end labelIndexForColorName 1023 | 1024 | 1025 | -- This method serves as a hook which gets called for every annotation with an annotation comment. 1026 | -- It can be used to transform the given annotation comment (which may contain custom markup syntax) 1027 | -- into a Keypoints-style format that's supported by this script. 1028 | on preprocessAnnotationComment(aComment) 1029 | return aComment -- comment out this line (i.e., prefix it with "--") if you want to use custom code below 1030 | 1031 | -- NOTE: below code is just an example that shows how you could transform the given annotation 1032 | -- comment so that it matches the Keypoints-style format used by this script. 1033 | 1034 | -- convert tags 1035 | -- - input: an annotation comment containing a separate line that starts with “Tags:” followed by 1036 | -- comma-separated values that represent the tags, e.g.: "Tags: some tag, another tag, test" 1037 | -- - output: an annotation comment containing a Keypoints-style metadata line with tags, 1038 | -- e.g.: "< @some tag @another tag @test" 1039 | set transformedLines to {} 1040 | set tagsLineRegex to "(?<=^|[\\r\\n])Tags:\\s*" 1041 | set tagDelimiterRegex to "(?<=^<|[\\r\\n]<)\\s+|\\s*,\\s*" 1042 | 1043 | repeat with aLine in paragraphs of aComment 1044 | if (KeypointsLib's regexMatch(aLine, tagsLineRegex)) is not "" then 1045 | set aLine to KeypointsLib's regexReplace(aLine, tagsLineRegex, "< ") 1046 | set aLine to KeypointsLib's regexReplace(aLine, tagDelimiterRegex, " @") 1047 | end if 1048 | copy aLine as text to end of transformedLines 1049 | end repeat 1050 | 1051 | set transformedString to KeypointsLib's mergeTextItems(transformedLines, linefeed) & linefeed 1052 | 1053 | return transformedString 1054 | end preprocessAnnotationComment 1055 | -------------------------------------------------------------------------------- /DEVONthink/DEVONthink_Notes_from_PDF_Annotations/DEVONthink_Notes_from_PDF_Annotations.scpt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/extracts/mac-scripting/07649f2dadc592a68048ec88c79e31e6037eefac/DEVONthink/DEVONthink_Notes_from_PDF_Annotations/DEVONthink_Notes_from_PDF_Annotations.scpt -------------------------------------------------------------------------------- /DEVONthink/DEVONthink_Notes_from_PDF_Annotations/DEVONthink_Notes_from_PDF_Annotations.scptd.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/extracts/mac-scripting/07649f2dadc592a68048ec88c79e31e6037eefac/DEVONthink/DEVONthink_Notes_from_PDF_Annotations/DEVONthink_Notes_from_PDF_Annotations.scptd.zip -------------------------------------------------------------------------------- /DEVONthink/DEVONthink_Notes_from_PDF_Annotations/README.md: -------------------------------------------------------------------------------- 1 | # DEVONthink Notes from PDF Annotations 2 | 3 | For each of the PDFs selected in DEVONthink, this script will iterate over its contained PDF annotations and create or update a Markdown record for each markup or text annotation. 4 | 5 | The URL of each Markdown record will be set to a deep link that directly points to the corresponding PDF annotation. I.e., clicking this deep link will open the associated PDF and scroll the corresponding PDF annotation into view. 6 | 7 | For each Markdown record, the script assigns a color label that matches your annotation's highlight color (see [Setup](#setup) below). 8 | 9 | The script recognizes some markup syntax in PDF annotation notes. This lets you specify the annotation's name/title and comment as well as its flagged status, star rating, tags and custom metadata. Example annotation note as supported by this script: 10 | 11 | ``` 12 | # Your title for this annotation 13 | 14 | Your comment about this annotation. 15 | 16 | < *** @tag @another tag @:flagged @:metadatakey:Some value 17 | ``` 18 | 19 | If a DOI was found for the PDF, the script can also fetch its bibliographic metadata and set the custom metadata and/or Finder comment of the Markdown records & their group accordingly. 20 | 21 | Starting with v1.3, the script can be triggered from a DEVONthink smart rule. This allows to automatically extract PDF annotations from imported or updated PDFs. 22 | 23 | For further details, please see the notes at the top of the script. 24 | 25 | 26 | ## Installation 27 | 28 | The precompiled & signed `.scptd` version of this script already includes the required scripting library and is ready to go. Just [download](https://github.com/extracts/mac-scripting/raw/master/DEVONthink/DEVONthink_Notes_from_PDF_Annotations/DEVONthink_Notes_from_PDF_Annotations.scptd.zip) the zipped `.scptd` package, then double click it to unzip. 29 | 30 | After unpacking the script, copy it to a suitable place, like the DEVONthink Scripts folder. To open this folder, activate DEVONthink, select the [Scripts menu](https://download.devontechnologies.com/download/devonthink/3.8.2/DEVONthink.help/Contents/Resources/pgs/menus-scripts.html) and choose "Open Scripts Folder". This will open the DEVONthink Scripts folder in the Finder. It is located at `~/Library/Application Scripts/com.devon-technologies.think3/Menu`. 31 | 32 | Alternatively, you could also copy the script to the system's Script menu folder. For an illustrated guide which describes how to enable and use the system's script menu, please see [iworkautomation.com: The script menu](https://iworkautomation.com/numbers/script-menu.html). 33 | 34 | If you've placed your script into the DEVONthink Scripts folder, you may also append a keyboard shortcut description (like `___Command-Shift-Alt-A`) to the script's name. You should then be able to run your script by pressing the specified keyboard shortcut. 35 | 36 | If you want the script to be triggered by a DEVONthink smart rule instead, please move the script into the DEVONthink smart rule scripts folder at `~/Library/Application Scripts/com.devon-technologies.think3/Smart Rules`. Then, in your smart rule, add an "Execute Script" action and choose "External" as well as your script from the dropdown menus. For more info, see the DEVONthink help on [Smart Rules](https://download.devontechnologies.com/download/devonthink/3.8.2/DEVONthink.help/Contents/Resources/pgs/automation-smartrules.html) and [Smart Rule Scripts](https://download.devontechnologies.com/download/devonthink/3.8.2/DEVONthink.help/Contents/Resources/pgs/automation-smartrulescripts.html). 37 | 38 | 39 | ## Setup 40 | 41 | Before running the script, do this once: Open the script in Script Editor and adjust the DEVONthink label <-> color mapping via the properties `label1` ... `label7`, then save the script again. If saving generates an error, please try again with [Script Debugger](https://latenightsw.com/sd8/download/) (which has a free "lite" mode). 42 | 43 | You may also want to check out some of the other script properties as these allow to customize the script, e.g. to enable updating of existing notes or automatic fetching of BibTeX data. 44 | 45 | 46 | ## Usage 47 | 48 | Before running the script, make sure that DEVONthink is running, and that you've selected one or more PDF records with PDF annotations in DEVONthink. 49 | 50 | To run the script, select its menu entry from the (DEVONthink or system's) Scripts menu, or press your keyboard shortcut. 51 | 52 | After the script has finished, you'll see a dialog with feedback on how many PDFs have been processed and how many note records have been created/updated. For each PDF, its annotation notes are stored within a DEVONthink group next to the PDF. 53 | 54 | Note that you can run the script multiple times with the same PDF record(s) selected in DEVONthink. On a subsequent run of the script, all notes that were newly created (or updated) will be selected. 55 | 56 | If the script was executed automatically from within a DEVONthink smart rule (which, in turn, may have been triggered by an Import or Save event), script feedback will be reported via a notification, and created/updated notes won't get selected. 57 | 58 | 59 | ## Discussion & Help: 60 | 61 | If you have further questions or issues w.r.t. this script, or want to discuss or request any features, you may do so in either of these forums: 62 | 63 | - https://discourse.devontechnologies.com/t/script-to-create-individual-markdown-notes-from-pdf-annotations/80987 64 | - https://github.com/extracts/mac-scripting/discussions 65 | 66 | 67 | ## Requirements 68 | 69 | This script requires macOS 10.14 (High Sierra) or greater, the [KeypointsScriptingLib](https://github.com/extracts/mac-scripting/tree/master/ScriptingLibraries/KeypointsScriptingLib) v1.5 or greater, 70 | and [DEVONthink Pro](https://www.devontechnologies.com/apps/devonthink) v3.x or greater (DEVONthink Pro v3.9 or greater will be required to have deep links to PDF annotations work correctly). 71 | 72 | 73 | ## Credits 74 | 75 | by Matthias Steffens, keypoints.app, mat(at)extracts(dot)de 76 | 77 | Thanks to: 78 | 79 | * mdbraber whose "[ASObjC script code to parse PDF annotations](https://discourse.devontechnologies.com/t/stream-annotations-from-your-pdf-reading-sessions-with-devonthink/70727/30)" gave inspiration for this script. 80 | * Rob Trew for his "[Sente 6 Notes to Devonthink](https://github.com/RobTrew/tree-tools/blob/master/DevonThink%20scripts/Sente6ToDevn73.applescript)" script. 81 | * Takaaki Naganoya, Piyomaru Software, for his [makeNSRect](http://piyocast.com/as/archives/643) handler. 82 | 83 | 84 | ## License 85 | 86 | This script is licensed under the MIT license. In short, you can do whatever you want with this script as long as you include the original copyright and license notice in any copy of the script/source. 87 | 88 | For more info, please see [MIT license](https://github.com/extracts/mac-scripting/blob/master/LICENSE). 89 | 90 | 91 | ## Release Notes 92 | 93 | ### v1.3 94 | 95 | * The script can now be triggered from a DEVONthink smart rule. This allows to automatically extract PDF annotations from imported or updated PDFs. 96 | * If available, the script now extracts a PDF annotation's creation date and sets the creation date of the created Markdown record accordingly. 97 | * When recreating a Markdown record from its PDF annotation, the record's ID-like alias now remains stable. 98 | * Fixed an issue where, upon completion, the script would only select created/updated Markdown records from the PDF that got processed last. 99 | 100 | ### v1.2 101 | 102 | * The script will now recognize & import PDF annotations of type "FreeText". Unlike ordinary text annotations that are displayed in a pop-up window, free text annotations are always visible. 103 | * For each Markdown record, the script will now add a sort identifier string to an `annotationorder` custom metadata field. This metadata field can be used in DEVONthink to sort annotations in the order they appear in the text of a PDF page. 104 | * Added hook method `preprocessAnnotationComment()` which gets called for every annotation with an annotation comment. This can be used to preprocess & transform the given annotation comment (which may contain [custom markup syntax](https://discourse.devontechnologies.com/t/script-to-create-individual-markdown-notes-from-pdf-annotations/80987/10)) into a Keypoints-style format that's supported by this script. 105 | 106 | ### v1.1 107 | 108 | * If a PDF has a DOI set in custom metadata this will be preferred over any DOI extracted from the PDF itself. 109 | * Fixed an issue where only custom attributes from the last metadata line were extracted from a PDF annotation note. 110 | * Fixed an issue where "…" was incorrectly appended when generating note names. 111 | 112 | ### v1.0 113 | 114 | Initial release. 115 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Matthias Steffens 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Papers3/Papers_To_Bookends/Papers_To_Bookends.app.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/extracts/mac-scripting/07649f2dadc592a68048ec88c79e31e6037eefac/Papers3/Papers_To_Bookends/Papers_To_Bookends.app.zip -------------------------------------------------------------------------------- /Papers3/Papers_To_Bookends/Papers_To_Bookends.applescript: -------------------------------------------------------------------------------- 1 | -- Papers to Bookends 2 | -- version 1.4, licensed under the MIT license 3 | 4 | -- by Matthias Steffens, keypointsapp.net, mat(at)extracts(dot)de 5 | 6 | -- Exports all publications selected in your Papers 3 library (incl. its primary PDFs) to Bookends. 7 | 8 | -- This script requires macOS 10.10 (Yosemite) or greater, the KeypointsScriptingLib v1.2 or 9 | -- greater, Papers 3.4.2 or greater, and Bookends 12.5.5 or greater. 10 | 11 | -- Besides the common publication metadata (supported by the RIS format), this export script will 12 | -- also transfer the following publication properties (if not disabled below): 13 | -- * rating 14 | -- * color label 15 | -- * flagged status 16 | -- * language 17 | -- * edition 18 | -- * citekey 19 | -- * "papers://…" link 20 | -- For the color label and flagged status, the script will add special keywords to the corresponding 21 | -- Bookends publication (these keywords can be customized below). 22 | -- For journal articles, the script will also transfer the publication's PMID and PMCID (if defined). 23 | 24 | -- NOTE: Before executing the app, make sure that your Papers and Bookends apps are running, 25 | -- and that you've selected all publications in your Papers library that you'd like to export to 26 | -- Bookends. Then run the script to start the export process. 27 | 28 | -- NOTE: Upon completion, Bookends will display a modal dialog reporting how many publications 29 | -- (and PDFs) were imported. If the reported number of imported publications is less than the 30 | -- number of publications selected in your Papers library, you may want to open Console.app and 31 | -- checkout your system's console log for any errors reported by the script. 32 | 33 | -- NOTE: Due to a Papers scripting bug, the PDFs exported via this script won't include any 34 | -- annotations that you've added in Papers. However, the below workaround allows you to also 35 | -- include your annotations when exporting publications from your Papers library to Bookends: 36 | 37 | -- To include annotations from your Papers library inside the exported PDFs, do this once (before 38 | -- you run this script): 39 | -- 1. Make sure that the default Bookends attachments folder exists: This is the "Attachments" 40 | -- folder inside the "Bookends" folder within your "Documents" folder. Alternatively, you 41 | -- can specify a different folder in the `attachmentsFolderPath` property (see below). 42 | -- 2. Select all publications in your Papers library that you want to export, then choose 43 | -- the "File > Export… > PDF Files and Media" menu command, and make sure that the 44 | -- "Include annotations" checkbox is checked (in the save dialog, you may have to click 45 | -- the "Options" button to see this option). 46 | -- 3. In the save dialog, choose the attachments folder from step 1, and click the "Export" 47 | -- button. 48 | -- This will export all primary PDFs of all selected publications into your attachments folder. 49 | -- When you then run this script, the PDFs in your attachments folder will be used for import 50 | -- into Bookends. 51 | 52 | 53 | -- ----------- you may edit the values of the properties below ------------------ 54 | 55 | -- Specifies whether the publication's flagged status shall be exported to Bookends (`true`) 56 | -- or not (`false`). If `true`, and if the publication was flagged in your Papers library, this script 57 | -- will add the string given in `flaggedKeyword` (see below) as a keyword to the newly created 58 | -- Bookends publication. 59 | property transferPapersFlags : true 60 | 61 | -- The keyword to be added to the newly created Bookends publication if the publication was 62 | -- flagged in your Papers library. 63 | property flaggedKeyword : "Papers_flagged" 64 | 65 | -- Specifies whether the publication's color label shall be exported to Bookends (`true`) or not 66 | -- (`false`). If `true`, and if the publication was marked in your Papers library with a color label, 67 | -- this script will add the color's name (prefixed with the string given in `papersLabelPrefix`, see 68 | -- below) as a keyword to the newly created Bookends publication. 69 | property transferPapersLabel : true 70 | 71 | -- The string that will be prepended to a Papers color label name in order to form a special keyword 72 | -- which will be added to a newly created Bookends publication if the publication was marked in your 73 | -- Papers library with a color label. For example, using the default prefix string, a Papers entry marked 74 | -- with a red color label would be tagged with "Papers_label_red" in Bookends. 75 | property papersLabelPrefix : "Papers_label_" 76 | 77 | -- Specifies whether the publication's "papers://…" link shall be exported to Bookends (`true`) 78 | -- or not (`false`). If `true` the "papers://…" link will be appended to the Bookends "Notes" field. 79 | property transferPapersLink : true 80 | 81 | -- Specifies whether the publication's citekey shall be exported to Bookends (`true`) 82 | -- or not (`false`). If `true` the Papers citekey will be written to the Bookends "Key" field. 83 | property transferPapersCitekey : true 84 | 85 | -- Specifies the path to the attachments folder. For each Papers publication that shall be exported, 86 | -- this script will check this folder for a matching file attachment. And if this folder contains a file 87 | -- which exactly matches the formatted name of the publication's primary PDF, this file will be used 88 | -- for import into Bookends. Otherwise, a new file copy will be exported from your Papers library. 89 | -- Note that the path must be given as a POSIX path, either absolute or relative to your home folder. 90 | -- Use an empty string ("") to have the script ask for the attachment folder upon first run. The folder 91 | -- path will be remembered until the script is recompiled. 92 | property attachmentsFolderPath : "~/Documents/Bookends/Attachments" 93 | 94 | -- ----------- usually, you don't need to edit anything below this line ----------- 95 | 96 | property attachmentsFolder : missing value 97 | property tempFolder : missing value 98 | 99 | use KeypointsLib : script "KeypointsScriptingLib" 100 | use scripting additions 101 | 102 | 103 | on run 104 | my setupAttachmentsFolder() 105 | my setupTempFolder() 106 | KeypointsLib's setupProgress("Importing selected Papers publications into Bookends library…") 107 | 108 | tell application id "com.mekentosj.papers3" 109 | set selectedPubs to selected publications of front library window 110 | if selectedPubs is not {} then 111 | set exportFilePath to (tempFolder as string) & "PapersToBookends.ris" 112 | my exportPublicationsAsRIS(selectedPubs, exportFilePath) 113 | delay 1 114 | set risRecords to my risRecordsFromFile(exportFilePath as alias) 115 | set {bookendsImportedIDs, bookendsImportedPDFs} to my exportToBookends(selectedPubs, risRecords) 116 | tell application "Bookends" 117 | activate 118 | display dialog "Imported publications: " & (count of bookendsImportedIDs) & linefeed & "Imported PDFs: " & (count of bookendsImportedPDFs) ¬ 119 | with title "Finished Importing Publications" with icon note buttons {"OK"} default button "OK" 120 | end tell 121 | else 122 | KeypointsLib's displayError("Nothing selected!", "Please select some publications in your Papers library for export into Bookends.", 15, true) 123 | end if 124 | end tell 125 | end run 126 | 127 | 128 | -- Exports the given list of publication items from your Papers 3 library as RIS to the specified file path 129 | on exportPublicationsAsRIS(pubList, exportFilePath) 130 | if pubList is {} then 131 | KeypointsLib's displayError("Couldn't export RIS file!", "No publications were given for export.", 15, true) 132 | else if exportFilePath is missing value or exportFilePath is "" then 133 | KeypointsLib's displayError("Couldn't export selected publications as RIS file!", "No export path provided.", 15, true) 134 | end if 135 | 136 | tell application id "com.mekentosj.papers3" 137 | export pubList as RIS to file exportFilePath 138 | end tell 139 | end exportPublicationsAsRIS 140 | 141 | 142 | -- Returns a list of RIS records from the given RIS file 143 | on risRecordsFromFile(risFileAlias) 144 | set risFileContents to KeypointsLib's readFromFile(risFileAlias) 145 | if risFileContents does not contain "TY - " then 146 | KeypointsLib's displayError("Couldn't read RIS file contents!", "The exported RIS file could not be read again.", 15, true) 147 | end if 148 | 149 | -- insert a unique delimiter between RIS records, and split on this delimiter 150 | set risFileContents to KeypointsLib's regexReplace(risFileContents, linefeed & "ER - " & linefeed & "+TY - ", linefeed & "ER - " & linefeed & "$$##SPLIT_DELIM##$$" & linefeed & "TY - ") 151 | set risFileRecords to KeypointsLib's splitText(risFileContents, "$$##SPLIT_DELIM##$$" & linefeed) 152 | 153 | return risFileRecords 154 | end risRecordsFromFile 155 | 156 | 157 | -- Takes a list of publication items from your Papers 3 library and a matching list of RIS records, and imports them into Bookends 158 | on exportToBookends(pubList, risRecordList) 159 | local aRISRecord 160 | set bookendsImportedIDs to {} 161 | set bookendsImportedPDFs to {} 162 | set pubCount to count of pubList 163 | set risRecordCount to count of risRecordList 164 | if pubCount is not equal to risRecordCount then 165 | KeypointsLib's displayError("Publications don't match RIS file contents!", "The count of publications to be exported doesn't match the number of records in the RIS file.", 15, true) 166 | end if 167 | KeypointsLib's setTotalStepsForProgress(pubCount) 168 | 169 | repeat with i from 1 to pubCount 170 | tell application id "com.mekentosj.papers3" 171 | set aPub to item i of pubList 172 | set pubType to resource type of aPub 173 | set pubName to title of aPub 174 | KeypointsLib's updateProgress(i, "Importing publication " & i & " of " & pubCount & " (\"" & pubName & "\").") 175 | 176 | set aRISRecord to item i of risRecordList 177 | 178 | -- remove file spec from RIS record since we provide our own file to Bookends below 179 | set aRISRecord to KeypointsLib's regexReplace(aRISRecord, linefeed & "L1 - file://.+", "") 180 | 181 | -- for books, convert the BT tag in the RIS record to TI so that Bookends 12.8.3 and earlier correctly recognizes the book's title 182 | set risType to KeypointsLib's regexMatch(aRISRecord, "(?<=^TY - ).+") 183 | if risType is "BOOK" then -- we check the type of the RIS record (instead of pubType) since this also catches eBooks etc 184 | set aRISRecord to KeypointsLib's regexReplace(aRISRecord, "(?<=" & linefeed & ")BT(?= - )", "TI") 185 | end if 186 | 187 | -- remove any abbreviated journal name from RIS record since Bookends will autocomplete this using its Journal Glossary 188 | if pubType is "Journal Article" then 189 | set pubHasFullJournalName to (KeypointsLib's regexMatch(aRISRecord, linefeed & "T2 - .+") is not "") 190 | if pubHasFullJournalName then 191 | set aRISRecord to KeypointsLib's regexReplace(aRISRecord, linefeed & "J2 - .+", "") 192 | end if 193 | end if 194 | 195 | set bookendsImportInfo to "" 196 | 197 | set aFile to primary file item of aPub 198 | set exportFile to false 199 | if aFile is not missing value then 200 | set missingPDF to (full path of aFile is "" or full path of aFile is missing value) 201 | if not missingPDF then 202 | set exportFile to true 203 | else 204 | KeypointsLib's logToSystemConsole(name of me, "PDF is missing for publication \"" & pubName & "\", importing metadata only...") 205 | end if 206 | end if 207 | if exportFile then -- export file & metadata 208 | set fileName to formatted file name of aFile 209 | if fileName is missing value then 210 | KeypointsLib's displayError("Couldn't get file name!", "The file at \"" & filePath & "\" could not be found.", 15, true) 211 | end if 212 | 213 | -- check if the attachments folder already contains an existing file with a matching name (if so, use that, else export a new copy) 214 | set pdfExportFilePath to (attachmentsFolder as string) & fileName 215 | if KeypointsLib's fileExistsAtFilePath(POSIX path of pdfExportFilePath) then 216 | set pdfExportFile to pdfExportFilePath as alias 217 | else 218 | -- NOTE: due to a scripting bug in Papers, annotations are not included when exporting the file (even if Papers is setup to do so) 219 | export {aPub} as PDF Files to file (tempFolder as string) 220 | set pdfExportFile to ((tempFolder as string) & fileName) as alias 221 | end if 222 | 223 | tell application "Bookends" to set bookendsImportInfo to «event PPRSADDA» (POSIX path of pdfExportFile) given «class RIST»:aRISRecord 224 | else -- export just metadata 225 | tell application "Bookends" to set bookendsImportInfo to «event PPRSADDA» given «class RIST»:aRISRecord 226 | end if 227 | 228 | set bookendsImportID to "" 229 | set bookendsImportedPDF to "" 230 | if bookendsImportInfo is not "" then 231 | set bookendsImportID to KeypointsLib's regexMatch(bookendsImportInfo, "^\\d+(?=" & linefeed & ")") 232 | if bookendsImportID is not "" then 233 | copy bookendsImportID to end of bookendsImportedIDs 234 | else 235 | KeypointsLib's logToSystemConsole(name of me, "Couldn't properly import publication \"" & pubName & "\". Bookends info: " & bookendsImportInfo) 236 | end if 237 | 238 | set bookendsImportedPDF to KeypointsLib's regexMatch(bookendsImportInfo, "(?<=\\d" & linefeed & ").+\\.pdf(?=$|" & linefeed & ")") 239 | if bookendsImportedPDF is not "" then copy bookendsImportedPDF to end of bookendsImportedPDFs 240 | else 241 | KeypointsLib's logToSystemConsole(name of me, "Couldn't properly import publication \"" & pubName & "\".") 242 | end if 243 | 244 | if bookendsImportID is not "" then 245 | try -- getting the json string may cause a -10000 error 246 | set pubJSON to json string of aPub 247 | on error errorText number errorNumber 248 | if errorNumber is not -128 then 249 | set pubJSON to missing value 250 | KeypointsLib's logToSystemConsole(name of me, "Couldn't properly import color label, language and/or edition for publication \"" & pubName & "\"." & linefeed & "Error: " & errorText & " (" & errorNumber & ")") 251 | end if 252 | end try 253 | 254 | -- set rating 255 | set rating to my rating of aPub 256 | if rating > 0 then 257 | tell application "Bookends" to «event PPRSSFLD» bookendsImportID given «class FLDN»:"rating", string:rating 258 | end if 259 | 260 | if transferPapersLabel and pubJSON is not missing value then -- set color label 261 | set papersLabel to KeypointsLib's regexMatch(pubJSON, "(?<=" & linefeed & " \"label\": ).+(?=,)") 262 | if papersLabel > 0 then 263 | -- TODO: set the Bookends color label directly (as of Bookends 12.8.3, this isn't supported yet) 264 | --set bookendsLabel to my bookendsLabelForPapersLabel(papersLabel) 265 | --tell application "Bookends" to «event PPRSSFLD» bookendsImportID given «class FLDN»:"colorlabel", string:bookendsLabel 266 | 267 | tell application "Bookends" 268 | set tags to «event PPRSRFLD» bookendsImportID given string:"keywords" 269 | if tags is not "" then set tags to tags & linefeed 270 | «event PPRSSFLD» bookendsImportID given «class FLDN»:"keywords", string:tags & papersLabelPrefix & my papersColorForPapersLabel(papersLabel) 271 | end tell 272 | end if 273 | end if 274 | 275 | if transferPapersFlags then -- set flagged 276 | set isFlagged to flagged of aPub 277 | if isFlagged then 278 | tell application "Bookends" 279 | set tags to «event PPRSRFLD» bookendsImportID given string:"keywords" 280 | if tags is not "" then set tags to tags & linefeed 281 | «event PPRSSFLD» bookendsImportID given «class FLDN»:"keywords", string:tags & flaggedKeyword 282 | end tell 283 | end if 284 | end if 285 | 286 | if pubJSON is not missing value then -- set language 287 | set language to KeypointsLib's regexMatch(pubJSON, "(?<=" & linefeed & " \"language\": \").+(?=\")") 288 | if language is not missing value and language is not "" then 289 | tell application "Bookends" to «event PPRSSFLD» bookendsImportID given «class FLDN»:"user7", string:language 290 | end if 291 | end if 292 | 293 | if pubJSON is not missing value then -- set edition 294 | set edition to KeypointsLib's regexMatch(pubJSON, "(?<=" & linefeed & " \"version\": \").+(?=\")") 295 | if edition is not missing value and edition is not "" then 296 | tell application "Bookends" to «event PPRSSFLD» bookendsImportID given «class FLDN»:"user2", string:edition 297 | end if 298 | end if 299 | 300 | if pubType is "Journal Article" then -- set PMID & PMCID 301 | set aPMID to pmid of aPub 302 | if aPMID is not missing value and aPMID is not "" then 303 | tell application "Bookends" to «event PPRSSFLD» bookendsImportID given «class FLDN»:"user18", string:aPMID 304 | end if 305 | 306 | set aPMCID to pmcid of aPub 307 | if aPMCID is not missing value and aPMCID is not "" then 308 | tell application "Bookends" to «event PPRSSFLD» bookendsImportID given «class FLDN»:"user16", string:aPMCID 309 | end if 310 | end if 311 | 312 | if transferPapersLink then -- append the "papers://…" link to the "notes" field 313 | set papersLink to item url of aPub 314 | if papersLink is not missing value and papersLink is not "" then 315 | tell application "Bookends" 316 | set notes to «event PPRSRFLD» bookendsImportID given string:"notes" 317 | if notes is not "" then set notes to notes & linefeed & linefeed 318 | «event PPRSSFLD» bookendsImportID given «class FLDN»:"notes", string:notes & papersLink 319 | end tell 320 | end if 321 | end if 322 | 323 | if transferPapersCitekey then -- set Papers citekey 324 | set papersCitekey to citekey of aPub 325 | if papersCitekey is not missing value and papersCitekey is not "" then 326 | tell application "Bookends" to «event PPRSSFLD» bookendsImportID given «class FLDN»:"user1", string:papersCitekey 327 | end if 328 | end if 329 | end if 330 | end tell 331 | end repeat 332 | 333 | KeypointsLib's updateProgress(pubCount, "Successfully imported " & (count of bookendsImportedIDs) & " publications with " & (count of bookendsImportedPDFs) & " PDFs.") 334 | 335 | return {bookendsImportedIDs, bookendsImportedPDFs} 336 | end exportToBookends 337 | 338 | 339 | -- Attempts to setup the attachments folder based on the POSIX path given in attachmentsFolderPath, or, 340 | -- if that path doesn't exist, asks the user to specify an attachments folder. Note that the folder path will 341 | -- be remembered until the script is recompiled. 342 | on setupAttachmentsFolder() 343 | if attachmentsFolderPath starts with "~/" then 344 | set homeFolderPath to POSIX path of (path to home folder) 345 | set attachmentsFolderPath to KeypointsLib's regexReplace(attachmentsFolderPath, "^~/", homeFolderPath) 346 | end if 347 | if KeypointsLib's fileExistsAtFilePath(attachmentsFolderPath) then 348 | set attachmentsFolder to POSIX file attachmentsFolderPath as alias 349 | else 350 | set attachmentsFolder to choose folder with prompt "Select the attachments folder containing any file attachments" 351 | end if 352 | end setupAttachmentsFolder 353 | 354 | 355 | -- Sets up the temporary folder. If the temp folder already exists, this will also remove any contained files. 356 | on setupTempFolder() 357 | set tempFolderContainer to path to temporary items 358 | set tempFolderPath to KeypointsLib's createNewFolder(POSIX path of tempFolderContainer, name of me) 359 | set tempFolder to POSIX file tempFolderPath as alias 360 | KeypointsLib's deleteFolderContents(tempFolder) -- deletes any existing items from the temp folder 361 | end setupTempFolder 362 | 363 | 364 | -- Returns the index of the Bookends color label corresponding to the given Papers label index. 365 | on bookendsLabelForPapersLabel(papersLabel) 366 | -- Papers label -> Bookends label (color name) 367 | -- 0 -> 0 (none) 368 | -- 1 -> 1 (red) 369 | -- 2 -> 2 (orange) 370 | -- 3 -> 7 (yellow) 371 | -- 4 -> 3 (green) 372 | -- 5 -> 4 (blue) 373 | -- 6 -> 5 (purple) 374 | -- 7 -> 6 (Papers: grey / Bookends: brown) 375 | set bookendsLabels to {1, 2, 7, 3, 4, 5, 6} 376 | 377 | if papersLabel ≥ 1 and papersLabel ≤ 7 then 378 | return item papersLabel of bookendsLabels 379 | else 380 | return 0 381 | end if 382 | end bookendsLabelForPapersLabel 383 | 384 | 385 | -- Returns the color name for the given Papers label index. 386 | on papersColorForPapersLabel(papersLabel) 387 | set papersColors to {"red", "orange", "yellow", "green", "blue", "purple", "grey"} 388 | 389 | if papersLabel ≥ 1 and papersLabel ≤ 7 then 390 | return item papersLabel of papersColors 391 | else 392 | return "none" 393 | end if 394 | end papersColorForPapersLabel 395 | -------------------------------------------------------------------------------- /Papers3/Papers_To_Bookends/Papers_To_Bookends.scpt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/extracts/mac-scripting/07649f2dadc592a68048ec88c79e31e6037eefac/Papers3/Papers_To_Bookends/Papers_To_Bookends.scpt -------------------------------------------------------------------------------- /Papers3/Papers_To_Bookends/README.md: -------------------------------------------------------------------------------- 1 | # Papers to Bookends 2 | 3 | Exports all publications selected in your Papers 3 library (incl. its primary PDFs) to Bookends. 4 | 5 | Besides the common publication metadata (supported by the RIS format), this export script will also transfer the following publication properties (if not disabled within the script): 6 | 7 | * rating 8 | * color label 9 | * flagged status 10 | * language 11 | * edition 12 | * citekey 13 | * "papers://…" link 14 | 15 | For the color label and flagged status, the script will add special keywords to the corresponding Bookends publication (these keywords can be customized within the script). For journal articles, the script will also transfer the publication's PMID and PMCID (if defined). 16 | 17 | ### Known Issues 18 | 19 | Due to a Papers scripting bug, the PDFs exported via this script won't include any annotations that you've added in Papers. However, the below workaround allows you to also include your annotations when exporting publications from your Papers library to Bookends: 20 | 21 | To include annotations from your Papers library inside the exported PDFs, do this once (before you run this script): 22 | 23 | 1. Make sure that the default Bookends attachments folder exists: This is the `Attachments` folder inside the `Bookends` folder within your `Documents` folder. Alternatively, you can specify a different folder in the `attachmentsFolderPath` property (inside the script). 24 | 25 | 2. Select all publications in your Papers library that you want to export, then choose the "File > Export… > PDF Files and Media" menu command, and make sure that the "Include annotations" checkbox is checked (in the save dialog, you may have to click the "Options" button to see this option). 26 | 27 | 3. In the save dialog, choose the attachments folder from step 1, and click the "Export" button. 28 | 29 | This will export all primary PDFs of all selected publications into your attachments folder. When you then run this script, the PDFs in your attachments folder will be used for import into Bookends. 30 | 31 | 32 | ## Installation 33 | 34 | The precompiled & signed `.app` version of this script already includes the required scripting library and is ready to go. Just [download](https://github.com/extracts/mac-scripting/raw/master/Papers3/Papers_To_Bookends/Papers_To_Bookends.app.zip) the zipped `.app` package, then double click it to unzip. 35 | 36 | 37 | ## Usage 38 | 39 | Before executing the app, make sure that your Papers and Bookends apps are running, and that you've selected all publications in your Papers library that you'd like to export to Bookends. Then double click the script app to start the export process. 40 | 41 | Upon completion, Bookends will display a modal dialog reporting how many publications (and PDFs) were imported. If the reported number of imported publications is less than the number of publications selected in your Papers library, you may want to open Console.app and checkout your system's console log for any errors reported by the script. 42 | 43 | 44 | ## Requirements 45 | 46 | This script requires macOS 10.10 (Yosemite) or greater, the [KeypointsScriptingLib](https://github.com/extracts/mac-scripting/tree/master/ScriptingLibraries/KeypointsScriptingLib) v1.2 or greater, [Papers](http://papersapp.com/mac) 3.4.2 or greater, and [Bookends](http://www.sonnysoftware.com/) 12.5.5 or greater. 47 | 48 | 49 | ## Credits 50 | 51 | by Matthias Steffens, keypointsapp.net, mat(at)extracts(dot)de 52 | 53 | 54 | ## License 55 | 56 | This script is licensed under the MIT license. In short, you can do whatever you want with this script as long as you include the original copyright and license notice in any copy of the script/source. 57 | 58 | For more info, please see [MIT license](https://github.com/extracts/mac-scripting/blob/master/LICENSE). 59 | 60 | 61 | ## Release Notes 62 | 63 | ### v1.4 64 | 65 | * Now gracefully handles papers whose primary PDF is missing from your Papers library: Instead of throwing an error, the script now ignores the missing PDF and just imports the publication metadata into Bookends. 66 | 67 | ### v1.3 68 | 69 | * Now requires the KeypointsScriptingLib v1.2 (or greater) which works around an AppleScriptObjC bug in macOS 10.13.0 (High Sierra) where `current application's NSNotFound` is returning the wrong value. 70 | 71 | ### v1.2 72 | 73 | * Worked around a Papers issue where getting the `json string` property of a publication can cause a `-10000` error. Instead of aborting the import process, the script now logs an error message to the system's console log and proceeds with the import. 74 | 75 | ### v1.1 76 | 77 | * For the RIS type `BOOK`, the script now converts the `BT` tag in the RIS record to `TI` so that Bookends 12.8.3 and earlier correctly recognizes the book's title. 78 | * Import errors are now logged to the system's console log. 79 | * The script now also transfers the publication's edition (if defined). 80 | 81 | ### v1.0 82 | 83 | Initial release. 84 | -------------------------------------------------------------------------------- /Papers3/Papers_To_DEVONthink/Papers_To_DEVONthink.app.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/extracts/mac-scripting/07649f2dadc592a68048ec88c79e31e6037eefac/Papers3/Papers_To_DEVONthink/Papers_To_DEVONthink.app.zip -------------------------------------------------------------------------------- /Papers3/Papers_To_DEVONthink/Papers_To_DEVONthink.applescript: -------------------------------------------------------------------------------- 1 | -- Papers to DEVONthink 2 | -- version 1.0, licensed under the MIT license 3 | 4 | -- by Matthias Steffens, keypointsapp.net, mat(at)extracts(dot)de 5 | 6 | -- Exports all notes & highlight annotations of all publications selected in your 7 | -- Papers 3 library to DEVONthink Pro. 8 | 9 | -- If not disabled within the script, the publication's primary PDF will be also 10 | -- indexed in DEVONthink Pro. 11 | 12 | -- This script requires macOS 10.10 (Yosemite) or greater, the KeypointsScriptingLib v1.2 or 13 | -- greater, Papers 3.4.2 or greater, and DEVONthink Pro 12.9.16 or greater. 14 | 15 | -- This export script will transfer the following annotation properties: 16 | -- * logical page number 17 | -- * quoted text 18 | -- * annotation type 19 | -- * creation date 20 | -- * annotation color 21 | 22 | -- In addition, these publication properties are also transferred: 23 | -- * formatted reference 24 | -- * cite key 25 | -- * keywords 26 | -- * color label 27 | -- * flagged status 28 | -- * "papers://…" link 29 | -- * BibTeX metadata 30 | 31 | -- The export of some of these properties can be disabled below. Example note as created by this script: 32 | 33 | (* 34 | Krell, A. et al., 2003. The biology and chemistry of land fast ice in the White Sea, Russia–A comparison 35 | of winter and spring conditions. Polar Biology, 26(11), pp.707–719. 36 | 37 | {Krell++2003WhiteSea} 38 | 39 | p.707: Sea ice therefore probably plays a major role in structuring the White Sea ecosystem, since it 40 | strongly alters the exchange of energy and material between water and atmosphere. -- Highlighted 26.11.2017 41 | *) 42 | 43 | -- NOTE: Before executing the app, make sure that your Papers and DEVONthink Pro apps are running, 44 | -- and that you've selected all publications in your Papers library that you'd like to export to 45 | -- DEVONthink Pro. Then run the script to start the export process. For each publication with a PDF, 46 | -- the script will create a group within the database or group you've selected in DEVONthink Pro, 47 | -- and populate it with RTF notes for each of your note or highlight annotations. 48 | 49 | -- NOTE: Upon completion, DEVONthink Pro will display a modal dialog reporting how many publications 50 | -- (and annotations) were imported. 51 | 52 | -- NOTE: If you again select the same database or group in DEVONthink Pro, you can run the 53 | -- script multiple times for the same PDF without creating duplicate notes. This may be useful 54 | -- if you want to add newly added annotations or update the label color for existing ones. 55 | -- However, if a note was modified in DEVONthink Pro, the script will leave it as is and create 56 | -- a duplicate note with the original note contents. 57 | 58 | 59 | -- ----------- you may edit the values of the properties below ------------------ 60 | 61 | -- Specifies whether the publication's primary PDF shall be indexed in DEVONthink Pro (`true`) 62 | -- or not (`false`). If `true`, this script will create an index entry for the publication's primary 63 | -- PDF next to any notes & highlight annotations exported by this script. 64 | property transferPapersPDF : true 65 | 66 | -- Specifies whether the publication's flagged status shall be exported to DEVONthink Pro (`true`) 67 | -- or not (`false`). If `true`, and if the publication was flagged in your Papers library, this script 68 | -- will mark the corresponding index entry for the publication's primary PDF as flagged. Note that 69 | -- this script won't flag the publication's group folder since this would flag all contained items. 70 | property transferPapersFlag : true 71 | 72 | -- Specifies whether the publication's keywords shall be transferred to DEVONthink Pro (`true`) 73 | -- or not (`false`). If `true`, this script will use the publication's keywords to set the tags of the 74 | -- group & PDF index entry that are created in DEVONthink Pro for the publication. 75 | property transferPapersKeywords : true 76 | 77 | -- Specifies whether the publication's BibTeX metadata shall be transferred to DEVONthink Pro 78 | -- (`true`) or not (`false`). If `true`, this script will add the publication's BibTeX metadata to the 79 | -- Spotlight comment field of the group that's created in DEVONthink Pro for the publication. Note 80 | -- that this script won't set the Spotlight comment field of the PDF index entry since this would cause 81 | -- DEVONthink Pro to also set the Spotlight comment of the target PDF file accordingly (which would 82 | -- overwrite any existing comments). 83 | property transferPapersBibTeX : true 84 | 85 | -- Specifies whether the publication's or annotation's color label shall be transferred to DEVONthink 86 | -- Pro (`true`) or not (`false`). If `true`, this script will mark the records created in DEVONthink 87 | -- Pro with an appropriate color label. 88 | property transferPapersLabel : true 89 | 90 | -- Specifies whether the publication's "papers://…" link shall be exported to DEVONthink Pro (`true`) 91 | -- or not (`false`). If `true`, the "papers://…" link will be written to the "URL" field of all records 92 | -- created in DEVONthink Pro. 93 | property transferPapersLink : true 94 | 95 | -- Specifies whether the publication's or annotation's creation date shall be exported to DEVONthink 96 | -- Pro (`true`) or not (`false`). If `true`, the creation date will be written to the "creation date" 97 | -- field of all groups and notes created in DEVONthink Pro. Note that this script won't touch the 98 | -- creation date of the created PDF index entry (for which DEVONthink displays the file's creation date). 99 | property transferPapersCreationDate : true 100 | 101 | -- ----------- usually, you don't need to edit anything below this line ----------- 102 | 103 | property exportedAnnotationsCount : 0 104 | 105 | use KeypointsLib : script "KeypointsScriptingLib" 106 | use scripting additions 107 | 108 | -- TODO: optionally transfer manual collections as tags 109 | -- TODO: offer an option to put the publication's formatted reference in the Spotlight comments instead 110 | 111 | 112 | -- adopt this routine to customize 113 | on run 114 | -- DEVONthink and Papers must be running for this script to work 115 | if not my checkAppsRunning() then return 116 | 117 | KeypointsLib's setupProgress("Exporting selected Papers publications to DEVONthink Pro…") 118 | 119 | tell application id "com.mekentosj.papers3" 120 | -- export the currently selected publications only 121 | set selectedPubs to selected publications of front library window 122 | 123 | -- filter the selection so that it only contains publications with a primary PDF 124 | set pdfPubs to my pubsWithPDF(selectedPubs) 125 | set pubCount to count of pdfPubs 126 | 127 | -- get current group/window in DEVONthink which should receive the notes 128 | set {dtContainer, dtWin} to my getDTTargetContainers() 129 | 130 | KeypointsLib's setTotalStepsForProgress(pubCount) 131 | set exportedAnnotationsCount to 0 132 | 133 | repeat with i from 1 to pubCount 134 | set aPub to item i of pdfPubs 135 | 136 | -- gather info for this publication 137 | set {pubRef, pubKey, pubTitle, pubLink, pubCreationDate} to {formatted reference, citekey, title, item url, creation date} of aPub 138 | set pubKeywords to name of every keyword item of aPub 139 | 140 | KeypointsLib's updateProgress(i, "Exporting publication " & i & " of " & pubCount & " (\"" & pubTitle & "\").") 141 | 142 | -- get all notes & highlight annotations for this publication 143 | set pubAnnotations to every annotation item of primary file item of aPub 144 | 145 | if transferPapersPDF or pubAnnotations is not {} then 146 | -- create a subfolder in DEVONthink (named like " - ") 147 | set folderName to pubKey & " - " & pubTitle 148 | set pubBibTeX to bibtex string of aPub 149 | set folderLocation to my createDTFolder(dtContainer, folderName, pubLink, pubCreationDate, pubKeywords, pubBibTeX) 150 | my transferPapersPublicationColor(folderLocation, aPub) 151 | 152 | if folderLocation is not missing value then 153 | -- index PDF file 154 | if transferPapersPDF then 155 | set pdfFile to primary file item of aPub 156 | set pdfPath to full path of pdfFile 157 | set isFlagged to flagged of aPub 158 | set indexRecord to my createDTIndexRecord(folderLocation, pdfPath, folderName, pubLink, pubKeywords, isFlagged) 159 | my transferPapersPublicationColor(indexRecord, aPub) 160 | end if 161 | 162 | -- export annotations 163 | my exportAnnotationsToDEVONthink(folderLocation, pubAnnotations, pubRef, pubKey, pubLink) 164 | else 165 | KeypointsLib's logToSystemConsole(name of me, "Couldn't export publication \"" & pubTitle & "\" since its group folder could not be created in DEVONthink.") 166 | end if 167 | end if 168 | end repeat 169 | end tell 170 | 171 | tell application id "DNtp" 172 | activate 173 | display dialog "Imported publications: " & pubCount & linefeed & "Imported annotations: " & exportedAnnotationsCount ¬ 174 | with title "Finished Import From Papers" with icon 2 buttons {"OK"} default button "OK" 175 | end tell 176 | end run 177 | 178 | 179 | -- Returns all publications from the given list of publications that have a primary PDF attached. 180 | on pubsWithPDF(pubList) 181 | tell application id "com.mekentosj.papers3" 182 | set allPubsWithPDF to {} 183 | repeat with aPub in pubList 184 | set pdfFile to primary file item of aPub 185 | if pdfFile is not missing value then 186 | copy contents of aPub to end of allPubsWithPDF 187 | end if 188 | end repeat 189 | return allPubsWithPDF 190 | end tell 191 | end pubsWithPDF 192 | 193 | 194 | -- Creates a new (rich text) record in DEVONthink for each of the given Papers note or highlight annotations. 195 | on exportAnnotationsToDEVONthink(folderLocation, pubAnnotations, pubRef, pubKey, pubLink) 196 | if folderLocation is missing value or pubAnnotations is missing value then return 197 | 198 | tell application id "com.mekentosj.papers3" 199 | repeat with anAnnotation in pubAnnotations 200 | if resource type of anAnnotation is not "Ink" then -- ink annotations aren't supported by this script 201 | set recordCreationDate to creation date of anAnnotation 202 | 203 | -- individual records have titles like "<CITEKEY> - <NOTE SUMMARY>" 204 | set annotationSummary to content summary of anAnnotation 205 | set recordName to pubKey & " - " & annotationSummary 206 | 207 | -- assemble formatted text for this note 208 | -- TODO: use a template mechanism for note formatting 209 | set recordContents to pubRef & linefeed & linefeed ¬ 210 | & "{" & pubKey & "}" & linefeed & linefeed ¬ 211 | & annotationSummary & linefeed & linefeed 212 | 213 | -- create a record for this note in DEVONthink 214 | set dtRecord to my createDTRecord(folderLocation, recordName, pubLink, recordContents, recordCreationDate) 215 | if dtRecord is not missing value then set exportedAnnotationsCount to exportedAnnotationsCount + 1 216 | 217 | -- set color label of DEVONthink record 218 | my transferPapersAnnotationColor(dtRecord, anAnnotation) 219 | end if 220 | end repeat 221 | end tell 222 | end exportAnnotationsToDEVONthink 223 | 224 | 225 | -- Sets the color label of the given DEVONthink record to the publication color label 226 | -- of the given Papers publication 227 | on transferPapersPublicationColor(dtRecord, papersPublication) 228 | if dtRecord is missing value or papersPublication is missing value then return 229 | 230 | set pubJSON to my jsonStringForPapersItem(papersPublication) 231 | 232 | if transferPapersLabel and pubJSON is not missing value then -- set color label 233 | set papersColorIndex to KeypointsLib's regexMatch(pubJSON, "(?<=" & linefeed & " \"label\": ).+(?=,)") 234 | if papersColorIndex > 0 then 235 | set dtLabel to my dtLabelForPapersPublicationColor(papersColorIndex) 236 | if dtLabel > 0 then 237 | tell application id "DNtp" to set label of dtRecord to dtLabel 238 | end if 239 | end if 240 | end if 241 | end transferPapersPublicationColor 242 | 243 | 244 | -- Sets the color label of the given DEVONthink record to the annotation color 245 | -- of the given Papers note or highlight annotation 246 | on transferPapersAnnotationColor(dtRecord, papersAnnotation) 247 | if dtRecord is missing value or papersAnnotation is missing value then return 248 | 249 | set noteJSON to my jsonStringForPapersItem(papersAnnotation) 250 | 251 | if transferPapersLabel and noteJSON is not missing value then -- set color label 252 | set papersColorIndex to KeypointsLib's regexMatch(noteJSON, "(?<=" & linefeed & " \"color\": ).+(?=,)") 253 | if papersColorIndex > 0 then 254 | set dtLabel to my dtLabelForPapersAnnotationColor(papersColorIndex) 255 | if dtLabel > 0 then 256 | tell application id "DNtp" to set label of dtRecord to dtLabel 257 | end if 258 | end if 259 | end if 260 | end transferPapersAnnotationColor 261 | 262 | 263 | -- Returns the contents of the `json string` property for the given Papers item. 264 | on jsonStringForPapersItem(papersItem) 265 | set jsonString to missing value 266 | try -- getting the json string may cause a -10000 error 267 | tell application id "com.mekentosj.papers3" to set jsonString to json string of papersItem 268 | on error errorText number errorNumber 269 | if errorNumber is not -128 then 270 | KeypointsLib's logToSystemConsole(name of me, "Couldn't fetch 'json string' property for papers item of type \"" & (class of papersItem) & "\"." & linefeed & "Error: " & errorText & " (" & errorNumber & ")") 271 | end if 272 | end try 273 | return jsonString 274 | end jsonStringForPapersItem 275 | 276 | 277 | -- Returns the index of the DEVONthink color label corresponding to the given Papers publication color index. 278 | on dtLabelForPapersPublicationColor(papersColorIndex) 279 | -- Papers publication color index (name) -> DEVONthink label index (name) 280 | -- 0 (none) -> 0 (none) 281 | -- 1 (red) -> 1 (red) 282 | -- 2 (orange) -> 5 (orange) 283 | -- 3 (yellow) -> 4 (yellow) 284 | -- 4 (green) -> 2 (green) 285 | -- 5 (blue) -> 3 (blue) 286 | -- 6 (purple) -> 7 (pink) // the "purple" Papers color label looks more like pink 287 | -- 7 (light gray) -> 6 (purple) // improper mapping! 288 | set dtLabels to {1, 5, 4, 2, 3, 7, 6} 289 | 290 | if papersColorIndex ≥ 1 and papersColorIndex ≤ 7 then 291 | return item papersColorIndex of dtLabels 292 | else 293 | return 0 294 | end if 295 | end dtLabelForPapersPublicationColor 296 | 297 | 298 | -- Returns the index of the DEVONthink color label corresponding to the given Papers annotation color index. 299 | on dtLabelForPapersAnnotationColor(papersColorIndex) 300 | -- Papers annotation color index (name) -> DEVONthink label index (name) 301 | -- used for highlight annotations: 302 | -- 0 (none) -> 0 (none) 303 | -- 1 (yellow) -> 4 (yellow) 304 | -- 2 (blue) -> 3 (blue) 305 | -- 3 (green) -> 2 (green) 306 | -- 4 (pink) -> 7 (pink) 307 | -- 5 (purple) -> 6 (purple) 308 | -- 6 (light gray) -> 5 (orange) // improper mapping! 309 | 310 | -- only used for ink annotations: 311 | -- 7 (orange) -> 5 (orange) 312 | -- 8 (red) -> 1 (red) 313 | -- 9 (black) -> 0 (none) 314 | set dtLabels to {4, 3, 2, 7, 6, 5, 5, 1, 0} 315 | 316 | if papersColorIndex ≥ 1 and papersColorIndex ≤ 9 then 317 | return item papersColorIndex of dtLabels 318 | else 319 | return 0 320 | end if 321 | end dtLabelForPapersAnnotationColor 322 | 323 | 324 | -- Finds the DEVONthink folder for this publication, or creates it if it doesn't exist. 325 | -- Credit: modified after script code by Rob Trew 326 | -- see https://github.com/RobTrew/tree-tools/blob/master/DevonThink%20scripts/Sente6ToDevn73.applescript 327 | on createDTFolder(dtContainer, folderName, folderURL, folderCreationDate, folderTags, folderComment) 328 | tell application id "DNtp" 329 | if (count of parents of dtContainer) is 0 then 330 | set dtLocation to (create location folderName in database of dtContainer) 331 | else 332 | set dtLocation to (create location (location of dtContainer & "/" & name of dtContainer & "/" & folderName) in database of dtContainer) 333 | end if 334 | 335 | if transferPapersLink and folderURL is not "" then 336 | set URL of dtLocation to folderURL 337 | end if 338 | 339 | if transferPapersCreationDate and folderCreationDate is not missing value then 340 | set creation date of dtLocation to folderCreationDate 341 | end if 342 | 343 | if transferPapersKeywords and folderTags is not {} then 344 | set tags of dtLocation to (tags of dtLocation) & folderTags -- in case the folder already exists 345 | end if 346 | 347 | if transferPapersBibTeX and folderComment is not "" and folderComment is not missing value then 348 | set comment of dtLocation to folderComment 349 | end if 350 | 351 | return dtLocation 352 | end tell 353 | end createDTFolder 354 | 355 | 356 | -- Creates a new (rich text) record in DEVONthink with the given text and returns it. 357 | -- Credit: modified after script code by Rob Trew 358 | -- see https://github.com/RobTrew/tree-tools/blob/master/DevonThink%20scripts/Sente6ToDevn73.applescript 359 | on createDTRecord(folderLocation, recordName, recordURL, recordText, recordCreationDate) 360 | tell application id "DNtp" 361 | set newRecordData to {type:rtf, rich text:recordText, name:recordName} 362 | 363 | if transferPapersLink and recordURL is not "" then 364 | set newRecordData to newRecordData & {URL:recordURL} 365 | end if 366 | 367 | if transferPapersCreationDate and recordCreationDate is not missing value then 368 | set newRecordData to newRecordData & {creation date:recordCreationDate} 369 | end if 370 | 371 | set newRecord to create record with newRecordData in folderLocation 372 | set aRecord to my deduplicatedDTRecord(newRecord) 373 | return aRecord 374 | end tell 375 | end createDTRecord 376 | 377 | 378 | -- Creates an indexed object for the given file path in DEVONthink. 379 | on createDTIndexRecord(folderLocation, filePath, recordName, recordURL, recordTags, isFlagged) 380 | tell application id "DNtp" 381 | set indexRecord to indicate filePath to folderLocation 382 | 383 | set aliases of indexRecord to recordName 384 | 385 | if transferPapersFlag and isFlagged then 386 | set state of indexRecord to isFlagged 387 | end if 388 | 389 | if transferPapersLink and recordURL is not "" then 390 | set URL of indexRecord to recordURL 391 | end if 392 | 393 | if transferPapersKeywords and recordTags is not {} then 394 | set tags of indexRecord to (tags of indexRecord) & recordTags 395 | end if 396 | 397 | set aRecord to my deduplicatedDTRecord(indexRecord) 398 | return aRecord 399 | end tell 400 | end createDTIndexRecord 401 | 402 | 403 | -- If the given record duplicates another in its group, discards the given record and 404 | -- returns the existing "duplicate" record, otherwise just returns the given record. 405 | on deduplicatedDTRecord(aRecord) 406 | tell application id "DNtp" 407 | set recordDuplicates to duplicates of aRecord 408 | if recordDuplicates is not {} then 409 | set recordLocation to location of aRecord 410 | repeat with aDuplicateRecord in recordDuplicates 411 | if location of aDuplicateRecord = recordLocation then 412 | delete record aRecord 413 | return aDuplicateRecord 414 | end if 415 | end repeat 416 | end if 417 | return aRecord 418 | end tell 419 | end deduplicatedDTRecord 420 | 421 | 422 | -- Checks if DEVONthink Pro and Papers are running. 423 | -- Credit: modified after script code by Rob Trew 424 | -- see https://github.com/RobTrew/tree-tools/blob/master/DevonThink%20scripts/Sente6ToDevn73.applescript 425 | on checkAppsRunning() 426 | tell application id "sevs" -- application "System Events" 427 | if (count of (processes where creator type = "DNtp")) < 1 then 428 | KeypointsLib's displayError("DEVONthink Pro not running!", "Please open DEVONthink Pro and select a target database or group, then run this script again.", 15, true) 429 | return false 430 | end if 431 | if (count of (processes where bundle identifier starts with "com.mekentosj.papers3")) < 1 then 432 | KeypointsLib's displayError("Papers 3 not running!", "Please open Papers 3 and select some publication(s), then run this script again.", 15, true) 433 | return false 434 | end if 435 | end tell 436 | return true 437 | end checkAppsRunning 438 | 439 | 440 | -- Gets the target window as well as the group currently selected in DEVONthink Pro. 441 | -- Credit: modified after script code by Rob Trew 442 | -- see https://github.com/RobTrew/tree-tools/blob/master/DevonThink%20scripts/Sente6ToDevn73.applescript 443 | on getDTTargetContainers() 444 | tell application id "DNtp" 445 | -- get the current group, if there is one 446 | set dtGroup to missing value 447 | with timeout of 1 second 448 | try 449 | set dtGroup to current group 450 | end try 451 | end timeout 452 | 453 | -- else, get the current database, if there is one 454 | try 455 | dtGroup 456 | on error 457 | set dtGroup to (root of database id 1) 458 | set dtWin to open window for record dtGroup 459 | return {dtGroup, dtWin} 460 | end try 461 | 462 | if dtGroup is missing value then 463 | set dtGroup to (root of database id 1) 464 | set dtWin to open window for record dtGroup 465 | return {dtGroup, dtWin} 466 | end if 467 | 468 | -- ensure that a window is open for this group 469 | set {dtDatabase, dtGroupID} to {database, id} of dtGroup 470 | set dtWindows to viewer windows where id of its root is dtGroupID and name of its root is name of dtDatabase 471 | if length of dtWindows < 1 then 472 | set dtWin to open window for record dtGroup 473 | else 474 | set dtWin to first item of dtWindows 475 | end if 476 | 477 | return {dtGroup, dtWin} 478 | end tell 479 | end getDTTargetContainers 480 | -------------------------------------------------------------------------------- /Papers3/Papers_To_DEVONthink/Papers_To_DEVONthink.scpt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/extracts/mac-scripting/07649f2dadc592a68048ec88c79e31e6037eefac/Papers3/Papers_To_DEVONthink/Papers_To_DEVONthink.scpt -------------------------------------------------------------------------------- /Papers3/Papers_To_DEVONthink/README.md: -------------------------------------------------------------------------------- 1 | # Papers to DEVONthink 2 | 3 | Exports all notes & highlight annotations of all publications selected in your Papers 3 library to DEVONthink Pro. 4 | 5 | If not disabled within the script, the publication's primary PDF will be also indexed in DEVONthink Pro. 6 | 7 | This export script will transfer the following annotation properties: 8 | 9 | * logical page number 10 | * quoted text 11 | * annotation type 12 | * creation date 13 | * annotation color 14 | 15 | In addition, these publication properties are also transferred: 16 | 17 | * formatted reference 18 | * cite key 19 | * keywords 20 | * color label 21 | * flagged status 22 | * "papers://…" link 23 | * BibTeX metadata 24 | 25 | The export of some of these properties can be disabled within the script. Example note as created by this script: 26 | 27 | ``` 28 | Krell, A. et al., 2003. The biology and chemistry of land fast ice in the White Sea, Russia–A comparison of winter and spring conditions. Polar Biology, 26(11), pp.707–719. 29 | 30 | {Krell++2003WhiteSea} 31 | 32 | p.707: Sea ice therefore probably plays a major role in structuring the White Sea ecosystem, since it strongly alters the exchange of energy and material between water and atmosphere. -- Highlighted 26.11.2017 33 | ``` 34 | 35 | 36 | ## Installation 37 | 38 | The precompiled & signed `.app` version of this script already includes the required scripting library and is ready to go. Just [download](https://github.com/extracts/mac-scripting/raw/master/Papers3/Papers_To_DEVONthink/Papers_To_DEVONthink.app.zip) the zipped `.app` package, then double click it to unzip. 39 | 40 | 41 | ## Usage 42 | 43 | Before executing the app, make sure that your Papers and DEVONthink Pro apps are running, and that you've selected all publications in your Papers library that you'd like to export to DEVONthink Pro. Then double click the script app to start the export process. 44 | 45 | For each publication with a PDF, the script will create a group within the database or group you've selected in DEVONthink Pro, and populate it with RTF notes for each of your note or highlight annotations. 46 | 47 | Upon completion, DEVONthink Pro will display a modal dialog reporting how many publications (and annotations) were imported. 48 | 49 | Note that, if you again select the same database or group in DEVONthink Pro, you can run the script multiple times for the same PDF without creating duplicate notes. This may be useful if you want to add newly added annotations or update the label color for existing ones. However, if a note was modified in DEVONthink Pro, the script will leave it as is and create a duplicate note with the original note contents. 50 | 51 | 52 | ## Requirements 53 | 54 | This script requires macOS 10.10 (Yosemite) or greater, the [KeypointsScriptingLib](https://github.com/extracts/mac-scripting/tree/master/ScriptingLibraries/KeypointsScriptingLib) v1.2 or greater, [Papers](http://papersapp.com/mac) 3.4.2 or greater, and [DEVONthink Pro](http://www.devontechnologies.com/products/devonthink/overview.html) 12.9.16 or greater. 55 | 56 | 57 | ## Credits 58 | 59 | by Matthias Steffens, keypointsapp.net, mat(at)extracts(dot)de 60 | 61 | Thanks to: 62 | 63 | * Rob Trew whose "[Papers2 to Devonthink](http://blog.devontechnologies.com/2011/03/1651/)" and "[Sente 6 Notes to Devonthink](https://github.com/RobTrew/tree-tools/blob/master/DevonThink%20scripts/Sente6ToDevn73.applescript)" scripts gave a lot of inspiration for this script. 64 | * Stephan Zellerhoff for providing code and suggestions on how to improve the script. 65 | 66 | 67 | ## License 68 | 69 | This script is licensed under the MIT license. In short, you can do whatever you want with this script as long as you include the original copyright and license notice in any copy of the script/source. 70 | 71 | For more info, please see [MIT license](https://github.com/extracts/mac-scripting/blob/master/LICENSE). 72 | 73 | 74 | ## Release Notes 75 | 76 | ### v1.0 77 | 78 | Initial release. 79 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mac-scripting - Automation scripts for macOS 2 | 3 | This repository contains an assortment of MIT-licensed AppleScript or JXA (JavaScript for Automation) scripts for various macOS applications. 4 | 5 | 6 | ## Getting Started with Scripting 7 | 8 | ### AppleScript 9 | 10 | The getting started guides [Scripting Papers 3 for Mac with AppleScript](https://extracts.github.io/mac-scripting/Papers3/Getting_Started) and [Scripting Bookends 13 with AppleScript](https://extracts.github.io/mac-scripting/Bookends/Getting_Started) contain a quick intro to AppleScript, and give concrete examples for scripting Papers 3 and Bookends 13, respectively. The guide's [Resources](https://extracts.github.io/mac-scripting/Bookends/Getting_Started#resources) section contains useful links to further info & tuturials about AppleScript. 11 | 12 | ### JavaScript for Automation 13 | 14 | During NSConference 2015, I've given a short introductory talk about [JavaScript For Automation: A Quick Intro To Its Use For Debugging](https://vimeo.com/124349703). Papers 3 was also used for the code examples in this talk. 15 | 16 | 17 | ## List of scripts 18 | 19 | See below for a summary of the scripts contained in this repository, and how to use them. For further info about a certain script, please see the script's README file in the script's subdirectory. 20 | 21 | 22 | ### Bookends 23 | 24 | #### Update group publications from attachment name 25 | 26 | A sample script for Bookends which shows how to extract information from attachment file names, and set publication metadata accordingly. [[More Info](https://github.com/extracts/mac-scripting/tree/master/Bookends/Update_group_publications_from_attachment_name)] [[Download](https://github.com/extracts/mac-scripting/raw/master/Bookends/Update_group_publications_from_attachment_name/Update_group_publications_from_attachment_name.app.zip)] 27 | 28 | 29 | ### DEVONthink 30 | 31 | #### DEVONthink Notes from PDF Annotations 32 | 33 | Creates individual Markdown notes for annotations contained in PDF(s) that are selected in DEVONthink. The script matches the PDF annotation highlight color to DEVONthink labels, supports markup in PDF annotation notes to set the note's title, flag, rating, tags & custom metadata, can auto-fetch bibliographic metadata, and creates deep links that directly point back to the PDF annotation. [[More Info](https://github.com/extracts/mac-scripting/tree/master/DEVONthink/DEVONthink_Notes_from_PDF_Annotations)] [[Download](https://github.com/extracts/mac-scripting/raw/master/DEVONthink/DEVONthink_Notes_from_PDF_Annotations/DEVONthink_Notes_from_PDF_Annotations.scptd.zip)] 34 | 35 | 36 | ### Papers 3 37 | 38 | #### Papers to Bookends 39 | 40 | Exports all publications selected in your Papers 3 library to Bookends. The script will also transfer the publication's primary PDF (if there's any) and can also transfer the publication's rating, color label, flagged status, language, edition, citekey, and "papers://..." link. [[More Info](https://github.com/extracts/mac-scripting/tree/master/Papers3/Papers_To_Bookends)] [[Download](https://github.com/extracts/mac-scripting/raw/master/Papers3/Papers_To_Bookends/Papers_To_Bookends.app.zip)] 41 | 42 | #### Papers to DEVONthink 43 | 44 | Exports all notes & highlight annotations of all publications selected in your Papers 3 library to DEVONthink Pro. The script will also index the publication's primary PDF in DEVONthink Pro, and can also transfer the annotation's color & creation date as well as the publication's formatted reference, citekey, keywords, color label, flagged status, "papers://..." link and BibTeX metadata. [[More Info](https://github.com/extracts/mac-scripting/tree/master/Papers3/Papers_To_DEVONthink)] [[Download](https://github.com/extracts/mac-scripting/raw/master/Papers3/Papers_To_DEVONthink/Papers_To_DEVONthink.app.zip)] 45 | 46 | 47 | ## FAQ 48 | 49 | ### How to execute AppleScript scripts? 50 | 51 | To open an AppleScript script (`.applescript`, `.scpt` or `.scptd`), drag it onto the app icon of the "Script Editor" app (which is located inside the `Utilities` folder within your `Applications` folder). Then click the "Run" button in Script Editor. 52 | 53 | Note that you can also save the script as a self-contained application (which you can double-click to execute it), or run it from the system's script menu (see below). 54 | 55 | ### How to use an AppleScript script with the system's script menu? 56 | 57 | The system's script menu provides a convenient way to execute your scripts. This script menu can be enabled in the Script Editor prefs. For an illustrated guide which describes how to enable and use the system's script menu, please see [iworkautomation.com: The script menu](https://iworkautomation.com/numbers/script-menu.html). 58 | 59 | ### What if a script's README states that the script requires a scripting library? 60 | 61 | If you download a precompiled `.app` or `.scptd` version of the script, then you won't need to do anything. In that case, any required scripting libraries are already included inside this signed script package. 62 | 63 | If you want to edit and/or compile a script from its text (`.applescript`) version, you'll need to download & install any required scripting libraries separately. Scripting libraries used by scripts in this repository are stored in the [ScriptingLibraries](https://github.com/extracts/mac-scripting/tree/master/ScriptingLibraries/) subdirectory. 64 | 65 | Perform the following steps to make a scripting library available to any scripts executed from within your current user account: 66 | 67 | 1. Find the correct scripting library in the [ScriptingLibraries](https://github.com/extracts/mac-scripting/tree/master/ScriptingLibraries/) subdirectory of this repository. 68 | 2. Download the scripting library's `.scptd` or `.zip` file to your desktop (or anywhere else). In case of a `.zip` file, double click it to unzip it. 69 | 3. In the Finder, choose "Go to Folder" from the "Go" menu and paste `~/Library/` into it, then hit Return. This opens the (hidden) `Library` folder inside your home folder. 70 | 4. Inside this `Library` folder, search for a folder named `Script Libraries`. If it doesn't exist, please create it with this exact name: `Script Libraries` 71 | 5. Now copy the `.scptd` file (which you got in step 2) into that `Script Libraries` folder. 72 | 73 | For more info about scripting libraries see [macosxautomation.com: AppleScript Libraries](https://macosxautomation.com/mavericks/libraries/index.html) and [Mac Automation Scripting Guide: Using Script Libraries](https://developer.apple.com/library/content/documentation/LanguagesUtilities/Conceptual/MacAutomationScriptingGuide/UseScriptLibraries.html#//apple_ref/doc/uid/TP40016239-CH36-SW1). 74 | -------------------------------------------------------------------------------- /ScriptingLibraries/KeypointsScriptingLib/KeypointsScriptingLib.scptd.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/extracts/mac-scripting/07649f2dadc592a68048ec88c79e31e6037eefac/ScriptingLibraries/KeypointsScriptingLib/KeypointsScriptingLib.scptd.zip -------------------------------------------------------------------------------- /ScriptingLibraries/KeypointsScriptingLib/README.md: -------------------------------------------------------------------------------- 1 | # Keypoints Scripting Library 2 | 3 | This script library contains useful AppleScript and AppleScriptObjC methods that can be used when 4 | writing scripts for the Mac. 5 | 6 | 7 | ## Installation & Usage 8 | 9 | For installation and usage information, see the FAQ entry "What if a script's README states that the script requires a scripting library?" in the [repository README](https://github.com/extracts/mac-scripting#what-if-a-scripts-readme-states-that-the-script-requires-a-scripting-library) and the linked web resources. 10 | 11 | 12 | ## Requirements 13 | 14 | This script library requires macOS 10.9 (Mavericks) or greater. You'll need at least macOS 10.10 (Yosemite) if you want to include some of its AppleScriptObjC handlers directly within your own scripts. 15 | 16 | 17 | ## Credits 18 | 19 | by Matthias Steffens, keypointsapp.net, mat(at)extracts(dot)de 20 | 21 | Some methods were taken (or modified after) sample methods from the eBook [Everyday AppleScriptObjC](http://www.macosxautomation.com/applescript/apps/everyday_book.html) 22 | by Shane Stanley which is a great introduction to AppleScriptObjC and which shows how to write 23 | AppleScriptObjC-based script libraries. 24 | 25 | 26 | ## License 27 | 28 | This script is licensed under the MIT license. In short, you can do whatever you want with this script 29 | as long as you include the original copyright and license notice in any copy of the script/source. 30 | 31 | For more info, please see [MIT license](https://github.com/extracts/mac-scripting/blob/master/LICENSE). 32 | 33 | 34 | ## Release Notes 35 | 36 | ### v1.5 37 | 38 | * The methods which generate Keypoints-style (date) IDs from a date with added millisecond precision now accept input to control the fixed number of milliseconds to be added. 39 | * Added these new methods: 40 | * `readJSONFromFileAtPath()` 41 | * `readJSONFromString()` 42 | * `pdfAnnotationInfo()` 43 | * `colorForAnnotation()` 44 | * `annotationText()` 45 | * `annotationBoundsByLine()` 46 | * `makeNSRect()` 47 | * `dateFromPDFDate()` 48 | * `dateFromNSDate()` 49 | * `colorNameForColor()` 50 | * `colorNameForHue()` 51 | * `expandAbbreviatedFilePath()` 52 | * `displayNotification()` 53 | * `displayMessage()` 54 | * `flattenList()` 55 | 56 | ### v1.4 57 | 58 | * Fixed issue in `customAttributesAndTagsFromKeypointsNote()` where only custom attributes from the last metadata line were returned. 59 | * Added these new methods: 60 | * `trimWhitespace()` 61 | * `itemsFromList:matchingRegex:negateResults:` 62 | * `matchDOI()` 63 | * `quotationLinesFromKeypointsNote()` 64 | * `metadataLinesFromKeypointsNote()` 65 | * `keypointsNoteWithoutMetadataLines()` 66 | * `setClipboard()` 67 | * `pasteIntoFrontApp()` 68 | 69 | ### v1.3 70 | 71 | * Improved the `valueForKey:inRecord:` & `fileExistsAtFilePath()` methods, and added more methods, e.g. to support the [DEVONthink Notes from PDF Annotations](https://github.com/extracts/mac-scripting/tree/master/DEVONthink/DEVONthink_Notes_from_PDF_Annotations) script. 72 | 73 | ### v1.2 74 | 75 | * Worked around an AppleScriptObjC bug in macOS 10.13.0 (High Sierra) where `current application's NSNotFound` is returning the wrong value. 76 | 77 | ### v1.1 78 | 79 | * The `-regexMatch`, `-regexReplace`, `-textReplace` and `-splitText` handlers now properly handle faulty (`nil`) input. 80 | 81 | ### v1.0 82 | 83 | Initial release. 84 | -------------------------------------------------------------------------------- /docs/Bookends/Getting_Started.applescript: -------------------------------------------------------------------------------- 1 | -- Sample scripts from the getting started guide 2 | -- "Scripting Bookends 13 with AppleScript" 3 | 4 | -- uncomment/comment the lines/blocks that you want to try out/ignore... 5 | -- block comments start with "(*" and end with "*)" 6 | -- single-line comments start with "--" 7 | 8 | 9 | -- APPLESCRIPT LANGUAGE ESSENTIALS 10 | 11 | -- -- Getting data 12 | 13 | (* 14 | tell application "Bookends" 15 | get library windows 16 | end tell 17 | *) 18 | 19 | --tell application "Bookends" to get library windows 20 | 21 | -- ------------------------------------------------------- 22 | 23 | -- -- Lists 24 | 25 | (* 26 | set myList to {1, 3, "Sonny Software", 5.5} 27 | 28 | get item 2 of myList 29 | get 2nd item of myList 30 | get second item of myList 31 | 32 | get last item of myList 33 | get items 2 thru 3 of myList 34 | *) 35 | 36 | (* 37 | tell application "Bookends" 38 | get properties of front library window 39 | end tell 40 | *) 41 | 42 | -- ------------------------------------------------------- 43 | 44 | -- -- Properties 45 | 46 | (* 47 | tell application "Bookends" 48 | get name of front library window 49 | end tell 50 | *) 51 | 52 | (* 53 | tell application "Bookends" 54 | get group recently viewed of front library window 55 | end tell 56 | *) 57 | 58 | (* 59 | tell application "Bookends" 60 | get publication items of group recently viewed of front library window 61 | end tell 62 | *) 63 | 64 | (* 65 | tell application "Bookends" 66 | tell front library window 67 | get publication items of group recently viewed 68 | end tell 69 | end tell 70 | *) 71 | 72 | (* 73 | tell application "Bookends" 74 | tell front library window 75 | get properties of first item of publication items of group recently viewed 76 | end tell 77 | end tell 78 | *) 79 | 80 | -- ------------------------------------------------------- 81 | 82 | -- -- Elements 83 | 84 | (* 85 | tell application "Bookends" 86 | tell front library window 87 | get attachment items of last item of publication items 88 | end tell 89 | end tell 90 | *) 91 | 92 | (* 93 | tell application "Bookends" 94 | tell front library window 95 | get attachment items of last publication item 96 | end tell 97 | end tell 98 | *) 99 | 100 | -- ------------------------------------------------------- 101 | 102 | -- -- Filtering 103 | 104 | (* 105 | tell application "Bookends" 106 | tell front library window 107 | get every publication item whose title contains "review" 108 | end tell 109 | end tell 110 | *) 111 | 112 | (* 113 | tell application "Bookends" 114 | tell front library window 115 | get every publication item whose title contains "review" and authors contains "Thomas, D" 116 | end tell 117 | end tell 118 | *) 119 | 120 | (* 121 | tell application "Bookends" 122 | tell front library window 123 | set pubList to every publication item 124 | set myList to {} 125 | repeat with i from 1 to count of pubList 126 | set aPub to item i of pubList 127 | if title of aPub contains "review" and authors of aPub contains "Thomas, D" then 128 | copy aPub to end of myList 129 | end if 130 | end repeat 131 | get myList 132 | end tell 133 | end tell 134 | *) 135 | 136 | 137 | -- ------------------------------------------------------- 138 | 139 | 140 | -- SCRIPTING BOOKENDS 141 | 142 | -- -- Object inheritance 143 | 144 | (* 145 | tell application "Bookends" 146 | tell front library window 147 | get properties of first attachment item of first publication item 148 | end tell 149 | end tell 150 | *) 151 | 152 | -- ------------------------------------------------------- 153 | 154 | -- -- Container hierarchy 155 | 156 | (* 157 | tell application "Bookends" 158 | tell front library window 159 | get group items 160 | end tell 161 | end tell 162 | *) 163 | 164 | --tell application "Bookends" to get every group item of front library window 165 | 166 | (* 167 | tell application "Bookends" 168 | tell front library window 169 | set pubList to publication items of every group item 170 | end tell 171 | end tell 172 | *) 173 | 174 | (* 175 | tell application "Bookends" 176 | tell front library window 177 | set {pubsByGroup, groupNames} to {publication item, name} of every group item 178 | end tell 179 | end tell 180 | *) 181 | 182 | (* 183 | tell application "Bookends" 184 | tell front library window 185 | get attachment items of publication items of every group item 186 | end tell 187 | end tell 188 | *) 189 | 190 | -- ------------------------------------------------------- 191 | 192 | -- -- Creating new objects 193 | 194 | (* 195 | tell application "Bookends" 196 | tell front library window 197 | make new publication item with properties {title:"My great new paper", publication date string:"2019/01/31"} 198 | end tell 199 | end tell 200 | *) 201 | 202 | (* 203 | tell application "Bookends" 204 | tell front library window 205 | make new publication item with properties {authors:"Mock, T" & linefeed & "Thomas, DN", citekey:"Mock+Thomas2005", doi:"10.1111/j.1462-2920.2005.00781.x", pmid:"15819843", title:"Recent advances in sea-ice microbiology", publication date string:"2005/03/21", journal:"Environ Microbiol", volume:"7(5)", pages:"605-619"} 206 | end tell 207 | end tell 208 | *) 209 | 210 | (* 211 | tell application "Bookends" 212 | tell front library window 213 | make new group item with properties {name:"New Group"} 214 | end tell 215 | end tell 216 | *) 217 | 218 | -- ------------------------------------------------------- 219 | 220 | -- -- Updating object properties 221 | 222 | (* 223 | tell application "Bookends" 224 | tell front library window 225 | set aPub to last publication item 226 | set title of aPub to "My great new paper" 227 | end tell 228 | end tell 229 | *) 230 | 231 | (* 232 | tell application "Bookends" 233 | tell front library window 234 | set rating of (every publication item whose keywords contains "review") to 4 235 | end tell 236 | end tell 237 | *) 238 | 239 | (* 240 | tell application "Bookends" 241 | tell front library window 242 | set pubList to every publication item whose keywords contains "review" 243 | repeat with aPub in pubList 244 | set rating of aPub to 4 245 | end repeat 246 | end tell 247 | end tell 248 | *) 249 | 250 | -- ------------------------------------------------------- 251 | 252 | -- -- Deleting objects 253 | 254 | (* 255 | tell application "Bookends" 256 | delete last publication item of front library window 257 | end tell 258 | *) 259 | 260 | (* 261 | tell application "Bookends" 262 | tell front library window 263 | delete (every publication item whose title is "My great new paper" and keywords is "") 264 | end tell 265 | end tell 266 | *) 267 | 268 | 269 | -- ------------------------------------------------------- 270 | 271 | 272 | -- USE CASES 273 | 274 | -- -- Importing publications 275 | 276 | (* 277 | tell application "Bookends" 278 | set pubList to quick add {"10.1084/jem.20052494", "20465544", "arXiv:0706.0001"} 279 | end tell 280 | *) 281 | 282 | (* 283 | tell application "Bookends" 284 | set pubList to quick add {"9781405185806"} to second library window 285 | end tell 286 | *) 287 | 288 | -- ------------------------------------------------------- 289 | 290 | -- -- Working with groups 291 | 292 | (* 293 | tell application "Bookends" 294 | tell front library window 295 | set allGroup to group all 296 | set attGroup to group attachments 297 | set hitsGroup to group hits 298 | set recentsGroup to group recently viewed 299 | end tell 300 | end tell 301 | *) 302 | 303 | (* 304 | tell application "Bookends" 305 | tell front library window 306 | set pubsWithFiles to publication items of group attachments 307 | end tell 308 | end tell 309 | *) 310 | 311 | (* 312 | tell application "Bookends" 313 | tell front library window 314 | set pubsWithMultipleFiles to publication items of group attachments whose attachments contains "\n" 315 | end tell 316 | end tell 317 | *) 318 | 319 | (* 320 | tell application "Bookends" 321 | set myGroups to group items of front library window 322 | end tell 323 | *) 324 | 325 | (* 326 | tell application "Bookends" 327 | set myGroups to group items of front library window 328 | end tell 329 | *) 330 | 331 | (* 332 | tell application "Bookends" 333 | tell front library window 334 | set timeDiff to (current date) - 2 * days -- last 2 days 335 | set recentlyAddedPubs to publication items where date added > timeDiff 336 | if recentlyAddedPubs is not {} then 337 | set groupName to "New Group" 338 | make new group item with properties {name:groupName} 339 | add recentlyAddedPubs to group item groupName 340 | end if 341 | end tell 342 | end tell 343 | *) 344 | 345 | (* 346 | tell application "Bookends" 347 | tell front library window 348 | set groupName to "New Group" 349 | set lastAddedPub to last publication item 350 | set idList to id of publication items of group item groupName 351 | if id of lastAddedPub is in idList then 352 | remove lastAddedPub from group item groupName 353 | end if 354 | end tell 355 | end tell 356 | *) 357 | 358 | -- ------------------------------------------------------- 359 | 360 | -- -- Formatting publications 361 | 362 | (* 363 | tell application "Bookends" 364 | tell front library window 365 | set formattedReference to format last publication item 366 | set the clipboard to formattedReference 367 | end tell 368 | end tell 369 | *) 370 | 371 | (* 372 | tell application "Bookends" 373 | tell front library window 374 | set pubsList to every publication item whose keywords contains "review" 375 | if pubsList is not {} then 376 | set formattedReferences to format pubsList using "Vancouver.fmt" 377 | end if 378 | end tell 379 | end tell 380 | *) 381 | 382 | -- ------------------------------------------------------- 383 | 384 | -- -- Finding publications 385 | 386 | (* 387 | tell application "Bookends" 388 | tell front library window 389 | set matchingPubs to sql search "keywords REGEX '(?i)primary producti(on|vity)'" 390 | end tell 391 | end tell 392 | *) 393 | 394 | (* 395 | tell application "Bookends" 396 | tell front library window 397 | set matchingPubs to sql search "authors REGEX '\\bNicol\\b,'" 398 | end tell 399 | end tell 400 | *) 401 | 402 | -- ------------------------------------------------------- 403 | 404 | -- -- Exporting publications 405 | 406 | (* 407 | tell application "Bookends" 408 | tell front library window 409 | set pubsList to publication items of group hits 410 | if pubsList is not {} then 411 | set bibtexRefs to format pubsList using "BibTeX.fmt" as BibTeX 412 | set the clipboard to bibtexRefs 413 | end if 414 | end tell 415 | end tell 416 | *) 417 | 418 | (* 419 | tell application "Bookends" 420 | set groupName to "My Papers" -- adopt to your needs 421 | set outFile to ((path to desktop from user domain) as string) & "Bibliography.ris" 422 | tell front library window 423 | set pubsList to publication items of group item groupName 424 | if pubsList is not {} then 425 | set risContent to format pubsList using "RIS.fmt" 426 | my writeTextToFile(outFile, risContent) 427 | end if 428 | end tell 429 | end tell 430 | 431 | --- Saves the given text to the specified file. Note that this will replace any existing file content. 432 | on writeTextToFile(aFile, theText) 433 | set aFileRef to open for access aFile with write permission 434 | set eof aFileRef to 0 435 | write theText to aFileRef as «class utf8» 436 | close access aFileRef 437 | end writeTextToFile 438 | *) 439 | -------------------------------------------------------------------------------- /docs/Bookends/Getting_Started.md: -------------------------------------------------------------------------------- 1 | # Scripting Bookends 13 with AppleScript – A Getting Started Guide 2 | 3 | ## About AppleScript 4 | 5 | [AppleScript](http://en.wikipedia.org/wiki/AppleScript) is an object-oriented scripting language with an English-like language syntax. It can be used on macOS to automate repetitive tasks, combine features from multiple scriptable applications, and to create complex workflows. 6 | 7 | 8 | 9 | ## AppleScript language essentials 10 | 11 | ### Getting data 12 | 13 | In order to get data from Bookends, you need to specify Bookends as the target of your AppleScript commands with a so called "tell statement". I.e., all your Bookends-related commands are wrapped within a `tell application "Bookends"` block: 14 | 15 | ```applescript 16 | tell application "Bookends" 17 | get library windows 18 | end tell 19 | ``` 20 | 21 | A tell statement specifies a default target for all commands contained within it. If you have only one command, you can also include the tell statement on the same line: 22 | 23 | ```applescript 24 | tell application "Bookends" to get library windows 25 | ``` 26 | 27 | In the above examples, we use the `get` command to fetch a list of all library windows that are currently open in Bookends. The `get` command has itself a target (in this case, `library windows`) which is the object that responds to the command. The command's target appears immediately next to the command and is also called the "direct parameter" of the command. 28 | 29 | 30 | ### Lists 31 | 32 | In AppleScript, a list is an ordered collection of values of any class. Lists are indicated with braces, and values in a list are separated by commas. As an example, the following command assigns a list with 4 items – two integer numbers, some text and a decimal ("real") number – to a variable named "myList": 33 | 34 | ```applescript 35 | set myList to {1, 3, "Sonny Software", 5.5} 36 | ``` 37 | 38 | The elements ([see below](#elements)) of lists are referred to as "items". You can refer to any list item by its item number. For example, for the above list, the following commands would all return the integer `3`: 39 | 40 | ```applescript 41 | get item 2 of myList 42 | get 2nd item of myList 43 | get second item of myList 44 | ``` 45 | 46 | Here's some other examples how list items can be accessed: 47 | 48 | ```applescript 49 | get last item of myList 50 | get items 2 thru 3 of myList 51 | ``` 52 | 53 | You can also use `front` or `back` to refer to the first or last element, respectively. This is often used when referring to application windows: 54 | 55 | ```applescript 56 | tell application "Bookends" 57 | get properties of front library window 58 | end tell 59 | ``` 60 | 61 | 62 | ### Properties 63 | 64 | A property of an object is a characteristic that has a single value and a label, such as the `name` property of a library window: 65 | 66 | ```applescript 67 | tell application "Bookends" 68 | get name of front library window 69 | end tell 70 | ``` 71 | 72 | Property values can be read/write or read only. The image below lists all properties (and example values) of the Bookends library window (an icon next to each property indicates a read-only property): 73 | 74 | ![](./Images/ScriptingBookends-LibraryWindow-Properties.png) 75 | 76 | A property may also represent another related object (which itself has properties). For example, this will return the "Recently Viewed" group which is one of the scriptable objects of the Bookends library window: 77 | 78 | ```applescript 79 | tell application "Bookends" 80 | get group recently viewed of front library window 81 | end tell 82 | ``` 83 | 84 | Using the `of` keyword you can get to any sub-properties: 85 | 86 | ```applescript 87 | tell application "Bookends" 88 | get publication items of group recently viewed of front library window 89 | end tell 90 | ``` 91 | 92 | Since the "Recently Viewed" group is [contained](#container-hierarchy) in the Bookends library window, the window can also be specified as the group's target (via a tell statement): 93 | 94 | ```applescript 95 | tell application "Bookends" 96 | tell front library window 97 | get publication items of group recently viewed 98 | end tell 99 | end tell 100 | ``` 101 | 102 | Instead of getting properties one by one, you can get all properties of an object at once: 103 | 104 | ```applescript 105 | tell application "Bookends" 106 | tell front library window 107 | get properties of first item of publication items of group recently viewed 108 | end tell 109 | end tell 110 | ``` 111 | 112 | The result is a "record". Here's part of the record resulting from the above command: 113 | 114 | ![](./Images/ScriptingBookends-PublicationItem-Properties.png) 115 | 116 | 117 | ### Records 118 | 119 | A record is an unordered collection of labeled properties (key-value pairs). A record appears in a script as a series of property definitions contained within braces and separated by commas. Each property definition consists of a unique label, a colon, and a value for the property. For example, the following is a record with three properties: 120 | 121 | ```applescript 122 | {title:"My great new paper", publication date string:"2019/01/31", rating:5} 123 | ``` 124 | 125 | 126 | ### Elements 127 | 128 | An element is an object contained within another object. An object can contain many elements or none, and the number of elements that it contains may change over time. As an example, a Bookends library window has related objects such as your groups and publications. A publication, in turn, has attachments. These are defined as elements. Here's an example that shows the elements defined for a library window: 129 | 130 | ![](./Images/ScriptingBookends-LibraryWindow-Elements.png) 131 | 132 | Similar to properties, you can access these elements using the `of` keyword: 133 | 134 | ```applescript 135 | tell application "Bookends" 136 | tell front library window 137 | get attachment items of last item of publication items 138 | end tell 139 | end tell 140 | ``` 141 | 142 | Alternatively, this more compact variant will also work: 143 | 144 | ```applescript 145 | tell application "Bookends" 146 | tell front library window 147 | get attachment items of last publication item 148 | end tell 149 | end tell 150 | ``` 151 | 152 | Elements will be returned as a [list](#lists) of objects. 153 | 154 | 155 | ### Filtering 156 | 157 | A filter specifies all objects in a container (such as a collection of elements) that match a condition, or test, specified by a Boolean expression. In effect, a filter reduces the number of objects in a container. 158 | 159 | As an example, instead of specifying every publication in your Bookends library, the following returns just those publications that have the word "review" in their title: 160 | 161 | ```applescript 162 | tell application "Bookends" 163 | tell front library window 164 | get every publication item whose title contains "review" 165 | end tell 166 | end tell 167 | ``` 168 | 169 | A term that uses the filter form is also known as a `whose` clause. You can use the words `where` or `that` as synonyms for `whose`. Note that the filter form works with application objects only. I.e., it cannot be used to filter the AppleScript objects `list`, `record`, or `text`. 170 | 171 | The return value of a filter reference form is a list of the objects that pass the test. If no objects pass the test, the list is an empty list: `{}`. You can also combine multiple tests within a single `whose` clause: 172 | 173 | ```applescript 174 | tell application "Bookends" 175 | tell front library window 176 | get every publication item whose title contains "review" and authors contains "Thomas, D" 177 | end tell 178 | end tell 179 | ``` 180 | 181 | A filter reference form could be rewritten in form of a `repeat` statement. For example, this is equivalent to the above: 182 | 183 | ```applescript 184 | tell application "Bookends" 185 | tell front library window 186 | set pubList to every publication item 187 | set myList to {} 188 | repeat with i from 1 to count of pubList 189 | set aPub to item i of pubList 190 | if title of aPub contains "review" and authors of aPub contains "Thomas, D" then 191 | copy aPub to end of myList 192 | end if 193 | end repeat 194 | get myList 195 | end tell 196 | end tell 197 | ``` 198 | 199 | While a `whose` clause is often the fastest way to obtain the desired information, more complex filtering may only be possible with a `repeat` statement. 200 | 201 | 202 | 203 | ## Scripting Bookends 204 | 205 | Bookends 13.2 and above features extensive AppleScript support that allows to easily fetch data from a Bookends library and to execute commands (such as import by identifier, or formatting of references). The scripting support in Bookends also enables you to create new references and to set its properties. 206 | 207 | 208 | ### Object inheritance 209 | 210 | The Bookends scripting interface exposes object classes for the most important elements of your Bookends library in a hierarchical data model: 211 | 212 | ![](./Images/ScriptingBookends-Inheritance.png) 213 | 214 | All publication-related object classes descend from an abstract base class (`library item`) which contains properties that are shared among all subclasses (like the object's ID). I.e., if you get all properties of a concrete object, e.g.: 215 | 216 | ```applescript 217 | tell application "Bookends" 218 | tell front library window 219 | get properties of first attachment item of first publication item 220 | end tell 221 | end tell 222 | ``` 223 | 224 | the superclass's properties (such as the ID) will be returned as well: 225 | 226 | ![](./Images/ScriptingBookends-AttachmentItem-Properties.png) 227 | 228 | Note that Bookends enforces unique names for any of your groups and attachments. Thus, group and attachment names can be also used as their IDs. 229 | 230 | 231 | ### AppleScript dictionary 232 | 233 | A dictionary is the part of a scriptable application that specifies the scripting terms it understands. The dictionary documents all object classes with its properties and elements, and may contain useful hints or sample code. You can choose "File > Open Dictionary" in Script Editor to display the dictionary of a scriptable application such as Bookends: 234 | 235 | ![](./Images/ScriptingBookends-BookendsDictionary-ScriptEditor.png) 236 | 237 | Here's the same dictionary in [Script Debugger](#tools), a third-party AppleScript debugger (which also offers a free "lite" version): 238 | 239 | ![](./Images/ScriptingBookends-BookendsDictionary-ScriptDebugger.png) 240 | 241 | 242 | ### Container hierarchy 243 | 244 | A container is an object that contains one or more objects or properties. The application target (identified by the `tell application "Bookends"` statement) constitutes the top-level container. The subordinate library window target contains elements for the main object classes: 245 | 246 | ![](./Images/ScriptingBookends-ContainerHierarchy.png) 247 | 248 | This allows you to easily get all objects of a certain class. For instance, this would return all of the groups that you've added to your Bookends library: 249 | 250 | ```applescript 251 | tell application "Bookends" 252 | tell front library window 253 | get group items 254 | end tell 255 | end tell 256 | ``` 257 | 258 | This is the same as above: 259 | 260 | ```applescript 261 | tell application "Bookends" to get every group item of front library window 262 | ``` 263 | 264 | Using the `of` keyword you can walk along the container hierarchy and follow the defined object relationships. For instance, this would return a list of publications by group, and assign the returned list to a variable: 265 | 266 | ```applescript 267 | tell application "Bookends" 268 | tell front library window 269 | set pubList to publication items of every group item 270 | end tell 271 | end tell 272 | ``` 273 | 274 | The result of the above command is a list of lists where the root items represent group items and the sub-items consist of the publication items in each group: 275 | 276 | ![](./Images/ScriptingBookends-Publications-ByGroup.png) 277 | 278 | Similarly, for each publication in a group, this would return a list of all attachments of that publication: 279 | 280 | ```applescript 281 | tell application "Bookends" 282 | tell front library window 283 | get attachment items of publication items of every group item 284 | end tell 285 | end tell 286 | ``` 287 | 288 | Use the following notation to simultaneously extract an equally sorted list of group names, and assign the lists to variables: 289 | 290 | ```applescript 291 | tell application "Bookends" 292 | tell front library window 293 | set {pubsByGroup, groupNames} to {publication item, name} of every group item 294 | end tell 295 | end tell 296 | ``` 297 | 298 | You can then walk these lists using a `repeat` statement. 299 | 300 | 301 | ### Creating new objects 302 | 303 | The easiest way of importing new publications is via the `quick add` command which will automatically fetch publication metadata for some given identifier(s) (see [Importing publications](#importing-publications) below). 304 | 305 | If you want to manually create a new publication, you can use the standard `make` command, e.g.: 306 | 307 | ```applescript 308 | tell application "Bookends" 309 | tell front library window 310 | make new publication item with properties {title:"My great new paper", publication date string:"2019/01/31"} 311 | end tell 312 | end tell 313 | ``` 314 | 315 | Here's a more detailed example for a real-world journal article: 316 | 317 | ```applescript 318 | tell application "Bookends" 319 | tell front library window 320 | make new publication item with properties {authors:"Mock, T" & linefeed & "Thomas, DN", citekey:"Mock+Thomas2005", doi:"10.1111/j.1462-2920.2005.00781.x", pmid:"15819843", title:"Recent advances in sea-ice microbiology", publication date string:"2005/03/21", journal:"Environ Microbiol", volume:"7(5)", pages:"605-619"} 321 | end tell 322 | end tell 323 | ``` 324 | 325 | Note that for string-based multi-value fields (like the `authors`, `editors`, `keywords` or `attachments` properties of the `publication item` class), individual values must be separated by a newline character. I.e., either use `\n`: 326 | 327 | ```applescript 328 | "Mock, T\nThomas, DN" 329 | ``` 330 | 331 | or, alternatively, use AppleScript's `linefeed` keyword: 332 | 333 | ```applescript 334 | "Mock, T" & linefeed & "Thomas, DN" 335 | ``` 336 | 337 | 338 | Similar to creating publications, you can use the `make` command to create a new static group: 339 | 340 | ```applescript 341 | tell application "Bookends" 342 | tell front library window 343 | make new group item with properties {name:"New Group"} 344 | end tell 345 | end tell 346 | ``` 347 | 348 | For more info on groups, see [Working with groups](#working-with-groups) below. 349 | 350 | 351 | ### Updating object properties 352 | 353 | You can set new values for any writable object property. For instance, this would update the title of the most recently added publication in your frontmost Bookends library: 354 | 355 | ```applescript 356 | tell application "Bookends" 357 | tell front library window 358 | set aPub to last publication item 359 | set title of aPub to "My great new paper" 360 | end tell 361 | end tell 362 | ``` 363 | 364 | You can also edit multiple objects at once. In most cases, you'll need to specify a `whose` clause to only update objects that match a certain condition. For instance, this command batch-updates the rating of all publications whose keywords field contains the string "review": 365 | 366 | ```applescript 367 | tell application "Bookends" 368 | tell front library window 369 | set rating of (every publication item whose keywords contains "review") to 4 370 | end tell 371 | end tell 372 | ``` 373 | 374 | Of course, you could also use a `repeat` statement instead: 375 | 376 | ```applescript 377 | tell application "Bookends" 378 | tell front library window 379 | set pubList to every publication item whose keywords contains "review" 380 | repeat with aPub in pubList 381 | set rating of aPub to 4 382 | end repeat 383 | end tell 384 | end tell 385 | ``` 386 | 387 | Please be aware that the update of objects is performed instantly and that this action cannot be undone. 388 | 389 | 390 | ### Deleting objects 391 | 392 | This will delete the most recently added publication from your frontmost Bookends library: 393 | 394 | ```applescript 395 | tell application "Bookends" 396 | delete last publication item of front library window 397 | end tell 398 | ``` 399 | 400 | As usual, you can also use a `whose` clause to specify the list of publications that a command will act on. For instance, in the below example, the checks for `title` and `keywords` ensure that only those publications get deleted which have the title "My great new paper" and which have no keywords assigned: 401 | 402 | ```applescript 403 | tell application "Bookends" 404 | tell front library window 405 | delete (every publication item whose title is "My great new paper" and keywords is "") 406 | end tell 407 | end tell 408 | ``` 409 | 410 | Please be aware that the deletion of objects is performed instantly and that this action cannot be undone. 411 | 412 | 413 | 414 | ## Use cases 415 | 416 | ### Importing publications 417 | 418 | Besides [creating new publications](#creating-new-objects) manually, the Bookends scripting interface features a `quick add` command which allows you to add publications via their identifier. Bookends will automatically fetch publication metadata for the given identifier(s) and, if successful, create new publication(s) with these metadata. 419 | 420 | This will import three publications (via their DOI, PMID and arXiv ID, respectively) into the front library window: 421 | 422 | ```applescript 423 | tell application "Bookends" 424 | set pubList to quick add {"10.1084/jem.20052494", "20465544", "arXiv:0706.0001"} 425 | end tell 426 | ``` 427 | 428 | You can pass any combination of the following identifiers to the `quick add` command: DOI, PMID, ISBN, JSTOR URL, or arXiv ID. Note that you must always pass a list, even if you specify only a single identifier. Also, you can target a specific library window explicitly: 429 | 430 | ```applescript 431 | tell application "Bookends" 432 | set pubList to quick add {"9781405185806"} to second library window 433 | end tell 434 | ``` 435 | 436 | 437 | ### Working with groups 438 | 439 | Bookends offers scripting access to its built-in groups via dedicated `library window` properties: 440 | 441 | ```applescript 442 | tell application "Bookends" 443 | tell front library window 444 | set allGroup to group all 445 | set attGroup to group attachments 446 | set hitsGroup to group hits 447 | set recentsGroup to group recently viewed 448 | end tell 449 | end tell 450 | ``` 451 | 452 | As mentioned [above](#properties), you can easily get all publications of a group, in this case all publications with attachments: 453 | 454 | ```applescript 455 | tell application "Bookends" 456 | tell front library window 457 | set pubsWithFiles to publication items of group attachments 458 | end tell 459 | end tell 460 | ``` 461 | 462 | As always, you can apply a [filter](#filtering) in order to fetch only a subset of the group's publications. For example, this will only return publications with multiple attachments: 463 | 464 | ```applescript 465 | tell application "Bookends" 466 | tell front library window 467 | set pubsWithMultipleFiles to publication items of group attachments whose attachments contains "\n" 468 | end tell 469 | end tell 470 | ``` 471 | 472 | Your own groups are available via the `group item` element of the `library window` class: 473 | 474 | ```applescript 475 | tell application "Bookends" 476 | set myGroups to group items of front library window 477 | end tell 478 | ``` 479 | 480 | We've already seen how the `make` command can be used to create new groups (see [creating new objects](#creating-new-objects) above). Here's how to create a new group with all publications that were added to your frontmost library within the last 2 days: 481 | 482 | ```applescript 483 | tell application "Bookends" 484 | tell front library window 485 | set timeDiff to (current date) - 2 * days -- last 2 days 486 | set recentlyAddedPubs to publication items where date added > timeDiff 487 | if recentlyAddedPubs is not {} then 488 | set groupName to "New Group" 489 | make new group item with properties {name:groupName} 490 | add recentlyAddedPubs to group item groupName 491 | end if 492 | end tell 493 | end tell 494 | ``` 495 | 496 | In the above example, we use the `add` command to add publications to the newly created group. Similarly, you can use the `remove` command to remove some publication(s) from a group. In the example below, we remove the most recently added publication from a group named "New Group": 497 | 498 | ```applescript 499 | tell application "Bookends" 500 | tell front library window 501 | set groupName to "New Group" 502 | set lastAddedPub to last publication item 503 | set idList to id of publication items of group item groupName 504 | if id of lastAddedPub is in idList then 505 | remove lastAddedPub from group item groupName 506 | end if 507 | end tell 508 | end tell 509 | ``` 510 | 511 | Note that the `remove` command will only remove the given publication(s) from the specified group, but the publication(s) will still remain in your library. 512 | 513 | 514 | ### Finding publications 515 | 516 | By now, you've already seen many examples of how to use a [filter](#filtering) to find all publications matching a condition or test. 517 | 518 | In addition to using a `whose` clause, Bookends also offers an `sql search` command which allows you to search for publications using an SQL query. For the below example, Bookends will return all publications whose keywords field contains either the string "primary production" or "primary productivity": 519 | 520 | ```applescript 521 | tell application "Bookends" 522 | tell front library window 523 | set matchingPubs to sql search "keywords REGEX '(?i)primary producti(on|vity)'" 524 | end tell 525 | end tell 526 | ``` 527 | 528 | In the above Regex query, the `(?i)` notation causes the search to be performed case insensitive. The `(…|…)` construct describes an alternation which, within the parens, matches either the expression to the left of the vertical line (`|`), or the right one. For more info on SQL/Regex searches, please see the Bookends user guide as well as [Regular Expression Metacharacters](http://userguide.icu-project.org/strings/regexp#TOC-Regular-Expression-Metacharacters). 529 | 530 | Here's another example which will return all publications whose authors field contains the lastname "Nicol": 531 | 532 | ```applescript 533 | tell application "Bookends" 534 | tell front library window 535 | set matchingPubs to sql search "authors REGEX '\\bNicol\\b,'" 536 | end tell 537 | end tell 538 | ``` 539 | 540 | Note that the name "Nicol" is wrapped within expressions that match a word boundary (`\b`). This ensures that the query matches only entire words and not occurrences in other words (such as "Nicols" or "Nicolson"). Also note that, in AppleScript scripts, any Regex expressions which use a backslash (`\`) must be escaped (`\\`). I.e., instead of `\b` you must write `\\b` in the script. 541 | 542 | 543 | ### Formatting publications 544 | 545 | The Bookends scripting interface allows you to output publications as formatted references. The example below will format the most recently added publication as a formatted reference, and copy it to the system clipboard—ready for insertion into another app: 546 | 547 | ```applescript 548 | tell application "Bookends" 549 | tell front library window 550 | set formattedReference to format last publication item 551 | set the clipboard to formattedReference 552 | end tell 553 | end tell 554 | ``` 555 | 556 | By default, the used format will correspond to the default format that you've specified in Bookends (see the "Biblio > Default Format" menu). 557 | 558 | You can also specify the desired output format explicitly, and you can format a list of publications at once: 559 | 560 | ```applescript 561 | tell application "Bookends" 562 | tell front library window 563 | set pubsList to every publication item whose keywords contains "thesis" 564 | if pubsList is not {} then 565 | set formattedReferences to format pubsList using "Vancouver.fmt" 566 | end if 567 | end tell 568 | end tell 569 | ``` 570 | 571 | The `format` command's `using` parameter accepts any format name supported by the Bookends formats manager (see the "Biblio > Formats Manager…" menu). 572 | 573 | 574 | ### Exporting publications 575 | 576 | You can also use the `format` command to output publications in bibliographic exchange formats (such as BibTeX, EndNote XML or RIS). This example will copy all publications of your "Hits" group as BibTeX to the system clipboard: 577 | 578 | ```applescript 579 | tell application "Bookends" 580 | tell front library window 581 | set pubsList to publication items of group hits 582 | if pubsList is not {} then 583 | set bibtexContent to format pubsList using "BibTeX.fmt" as BibTeX 584 | set the clipboard to bibtexContent 585 | end if 586 | end tell 587 | end tell 588 | ``` 589 | 590 | Here's a more advanced example which will export all publications contained in a group named "My Papers" as RIS to a file (named "Bibliography.ris") on your desktop: 591 | 592 | ```applescript 593 | tell application "Bookends" 594 | set groupName to "My Papers" -- adopt to your needs 595 | set outFile to ((path to desktop from user domain) as string) & "Bibliography.ris" 596 | tell front library window 597 | set pubsList to publication items of group item groupName 598 | if pubsList is not {} then 599 | set risContent to format pubsList using "RIS.fmt" 600 | my writeTextToFile(outFile, risContent) 601 | end if 602 | end tell 603 | end tell 604 | 605 | --- Saves the given text to the specified file. Note that this will replace any existing file content. 606 | on writeTextToFile(aFile, theText) 607 | set aFileRef to open for access aFile with write permission 608 | set eof aFileRef to 0 609 | write theText to aFileRef as «class utf8» 610 | close access aFileRef 611 | end writeTextToFile 612 | ``` 613 | 614 | The above example uses a dedicated handler (`writeTextToFile()`) to write the generated RIS content to a text file. For more info on script handlers, see Apple`s [Mac Automation Scripting Guide 615 | ](https://developer.apple.com/library/archive/documentation/LanguagesUtilities/Conceptual/MacAutomationScriptingGuide/UseHandlersFunctions.html). 616 | 617 | 618 | 619 | ## Resources 620 | 621 | * [AppleScript Language Guide](https://developer.apple.com/library/mac/documentation/AppleScript/Conceptual/AppleScriptLangGuide/introduction/ASLR_intro.html) 622 | 623 | Apple's guide to the AppleScript language gives a good overview of the language and describes its lexical conventions, syntax, keywords, and other elements in detail. 624 | 625 | * [AppleScript @ Wikipedia](http://en.wikipedia.org/wiki/AppleScript) 626 | 627 | The Wikipedia page for AppleScript features also a good summary of the language essentials such as data types, conditionals, loops, handlers, etc. 628 | 629 | * [Mac OS X Automation](http://macosxautomation.com/applescript/) 630 | 631 | A website with useful AppleScript resources ranging from tutorials (such as a [step-by-step AppleScript tutorial](http://macosxautomation.com/applescript/firsttutorial/index.html)), [script examples](http://macosxautomation.com/applescript/learn.html) and links to further [online resources about AppleScript](http://macosxautomation.com/applescript/resources.html). 632 | 633 | * [AppleScript for Absolute Starters](http://fischer-bayern.de/as/as4as/AS4AS_e.pdf) 634 | 635 | A free eBook by Bert Altenburg published in 2003 that is still a useful introduction to AppleScript. 636 | 637 | * [Some AppleScript tips](http://nathangrigg.net/2012/06/some-applescript-tips/) 638 | 639 | A good compilation of AppleScript tips by Nathan Grigg that explain, for instance, how to specify files in AppleScript, how to run an AppleScript from the command line, or how to pass variables by reference. 640 | 641 | * Since OS X 10.6, AppleScript allows to call Cocoa methods from within AppleScript scripts. The aforementioned [Mac OS X Automation](http://macosxautomation.com/applescript/) web site has a good compilation of [resources and tools for AppleScriptObjC](http://macosxautomation.com/applescript/apps/index.html). 642 | 643 | 644 | 645 | ## Tools 646 | 647 | * [Script Debugger](http://www.latenightsw.com) is a powerful third-party AppleScript debugger (which also offers a free "lite" version). Besides full debugging capabilities, Script Debugger features advanced dictionary viewers. It also offers unique value explorers that allow you to dynamically inspect the objects and properties offered by a scriptable application. Examples of its dictionary viewers and value explorers can be seen in some of the screenshots above. 648 | 649 | -------------------------------------------------------------------------------- /docs/Bookends/Getting_Started.scpt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/extracts/mac-scripting/07649f2dadc592a68048ec88c79e31e6037eefac/docs/Bookends/Getting_Started.scpt -------------------------------------------------------------------------------- /docs/Bookends/Images/ScriptingBookends-AttachmentItem-Properties.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/extracts/mac-scripting/07649f2dadc592a68048ec88c79e31e6037eefac/docs/Bookends/Images/ScriptingBookends-AttachmentItem-Properties.png -------------------------------------------------------------------------------- /docs/Bookends/Images/ScriptingBookends-BookendsDictionary-ScriptDebugger.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/extracts/mac-scripting/07649f2dadc592a68048ec88c79e31e6037eefac/docs/Bookends/Images/ScriptingBookends-BookendsDictionary-ScriptDebugger.png -------------------------------------------------------------------------------- /docs/Bookends/Images/ScriptingBookends-BookendsDictionary-ScriptEditor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/extracts/mac-scripting/07649f2dadc592a68048ec88c79e31e6037eefac/docs/Bookends/Images/ScriptingBookends-BookendsDictionary-ScriptEditor.png -------------------------------------------------------------------------------- /docs/Bookends/Images/ScriptingBookends-ContainerHierarchy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/extracts/mac-scripting/07649f2dadc592a68048ec88c79e31e6037eefac/docs/Bookends/Images/ScriptingBookends-ContainerHierarchy.png -------------------------------------------------------------------------------- /docs/Bookends/Images/ScriptingBookends-Inheritance.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/extracts/mac-scripting/07649f2dadc592a68048ec88c79e31e6037eefac/docs/Bookends/Images/ScriptingBookends-Inheritance.png -------------------------------------------------------------------------------- /docs/Bookends/Images/ScriptingBookends-LibraryWindow-Elements.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/extracts/mac-scripting/07649f2dadc592a68048ec88c79e31e6037eefac/docs/Bookends/Images/ScriptingBookends-LibraryWindow-Elements.png -------------------------------------------------------------------------------- /docs/Bookends/Images/ScriptingBookends-LibraryWindow-Properties.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/extracts/mac-scripting/07649f2dadc592a68048ec88c79e31e6037eefac/docs/Bookends/Images/ScriptingBookends-LibraryWindow-Properties.png -------------------------------------------------------------------------------- /docs/Bookends/Images/ScriptingBookends-PublicationItem-Properties.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/extracts/mac-scripting/07649f2dadc592a68048ec88c79e31e6037eefac/docs/Bookends/Images/ScriptingBookends-PublicationItem-Properties.png -------------------------------------------------------------------------------- /docs/Bookends/Images/ScriptingBookends-Publications-ByGroup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/extracts/mac-scripting/07649f2dadc592a68048ec88c79e31e6037eefac/docs/Bookends/Images/ScriptingBookends-Publications-ByGroup.png -------------------------------------------------------------------------------- /docs/Papers3/Getting_Started.md: -------------------------------------------------------------------------------- 1 | # Scripting Papers 3 for Mac with AppleScript – A Getting Started Guide 2 | 3 | ## About AppleScript 4 | 5 | [AppleScript](http://en.wikipedia.org/wiki/AppleScript) is an object-oriented scripting language with an English-like language syntax. It can be used on OS X to automate repetitive tasks, combine features from multiple scriptable applications, and to create complex workflows. 6 | 7 | 8 | ## AppleScript language essentials 9 | 10 | ### Getting data 11 | 12 | In order to get data from Papers, you need to specify Papers as the target of your AppleScript commands with a so called "tell statement". I.e., all your Papers-related commands are wrapped within a `tell application "Papers"` block: 13 | 14 | ```applescript 15 | tell application "Papers" 16 | get publication items 17 | end tell 18 | ``` 19 | 20 | A tell statement specifies a default target for all commands contained within it. If you have only one command, you can also include the tell statement on the same line: 21 | 22 | ```applescript 23 | tell application "Papers" to get publication items 24 | ``` 25 | 26 | In the above examples, we use the `get` command to fetch a list of all publications in the current library. The `get` command has itself a target (in this case, `publication items`) which is the object that responds to the command. The command's target appears immediately next to the command and is also called the "direct parameter" of the command. 27 | 28 | 29 | ### Lists 30 | 31 | In AppleScript, a list is an ordered collection of values of any class. Lists are indicated with braces, and values in a list are separated by commas. As an example, the following command assigns a list with 4 items – two integer numbers, some text and a decimal ("real") number – to a variable named "myList": 32 | 33 | ```applescript 34 | set myList to {1, 7, "Beethoven", 4.5} 35 | ``` 36 | 37 | The elements ([see below](#elements)) of lists are referred to as "items". You can refer to any list item by its item number. For example, for the above list, the following commands would all return the integer `7`: 38 | 39 | ```applescript 40 | get item 2 of myList 41 | get 2nd item of myList 42 | get second item of myList 43 | ``` 44 | 45 | Here's some other examples how list items can be accessed: 46 | 47 | ```applescript 48 | get last item of myList 49 | get items 2 thru 3 of myList 50 | ``` 51 | 52 | You can also use `front` or `back` to refer to the first or last element, respectively. This is often used when referring to application windows: 53 | 54 | ```applescript 55 | tell application "Papers" 56 | get properties of front library window 57 | end tell 58 | ``` 59 | 60 | 61 | ### Properties 62 | 63 | A property of an object is a characteristic that has a single value and a label, such as the `title` property of a publication: 64 | 65 | ```applescript 66 | tell application "Papers" 67 | get title of first item of publication items 68 | end tell 69 | ``` 70 | 71 | Property values can be read/write or read only. The picture below lists all properties (and example values) of the Papers library window (an icon next to each property indicates a read-only property): 72 | 73 | ![](./Images/ScriptingPapers-LibraryWindow-Properties.png) 74 | 75 | You can get all properties of an object at once: 76 | 77 | ```applescript 78 | tell application "Papers" 79 | get properties of first item of publication items 80 | end tell 81 | ``` 82 | 83 | The result is a "record". Here's part of the record resulting from the above command: 84 | 85 | ![](./Images/ScriptingPapers-PublicationItem-Properties.png) 86 | 87 | 88 | 89 | ### Records 90 | 91 | A record is an unordered collection of labeled properties (key-value pairs). A record appears in a script as a series of property definitions contained within braces and separated by commas. Each property definition consists of a unique label, a colon, and a value for the property. For example, the following is a record with three properties: 92 | 93 | ```applescript 94 | {title:"My great new paper", publication year:"2012", my rating:5} 95 | ``` 96 | 97 | 98 | ### Elements 99 | 100 | An element is an object contained within another object. An object can contain many elements or none, and the number of elements that it contains may change over time. The publications in your Papers library usually have related objects such as the publication's authors, editors, keywords or supplemental files. These are defined as elements. Here's an example that shows the elements defined for a publication: 101 | 102 | ![](./Images/ScriptingPapers-PublicationItem-Elements.png) 103 | 104 | Similar to properties, you can access these elements using the `of` keyword: 105 | 106 | ```applescript 107 | tell application "Papers" 108 | get author items of first item of publication items 109 | end tell 110 | ``` 111 | 112 | 113 | ### Filtering 114 | 115 | A filter specifies all objects in a container (such as a collection of objects/elements) that match a condition, or test, specified by a Boolean expression. In effect, a filter reduces the number of objects in a container. 116 | 117 | As an example, instead of specifying every publication in your Papers library, the following returns just those publications that have the word "review" in their title: 118 | 119 | ```applescript 120 | tell application "Papers" 121 | get every publication item whose title contains "review" 122 | end tell 123 | ``` 124 | 125 | A term that uses the filter form is also known as a `whose` clause. You can use the words `where` or `that` as synonyms for `whose`. Note that the filter form works with application objects only. I.e., it cannot be used to filter the AppleScript objects `list`, `record`, or `text`. 126 | 127 | The return value of a filter reference form is a list of the objects that pass the test. If no objects pass the test, the list is an empty list: `{}`. You can also combine multiple tests within a single `whose` clause: 128 | 129 | ```applescript 130 | tell application "Papers" 131 | get every publication item whose title contains "review" and author names contains "Thomas" 132 | end tell 133 | ``` 134 | 135 | A filter reference form could be rewritten in form of a `repeat` statement. For example, this is equivalent to the above: 136 | 137 | ```applescript 138 | tell application "Papers" 139 | set pubList to every publication item 140 | set myList to {} 141 | repeat with i from 1 to count of pubList 142 | set aPub to item i of pubList 143 | if title of aPub contains "review" and author names of aPub contains "Thomas" then 144 | copy aPub to end of myList 145 | end if 146 | end repeat 147 | get myList 148 | end tell 149 | ``` 150 | 151 | While a `whose` clause is often the fastest way to obtain the desired information, more complex filtering may only be possible with a `repeat` statement. 152 | 153 | 154 | ## Scripting Papers 155 | 156 | Papers 3 features extensive AppleScript support that allows to easily fetch data from a Papers library and to execute commands (such as import, export or matching of publications). The scripting support in Papers also enables you to create new data and to set its most important properties. You can also set the current selection or change interface-related properties such as the current view mode. 157 | 158 | 159 | ### Object inheritance 160 | 161 | The Papers scripting interface exposes object classes for the most important elements of your Papers library in a hierarchical data model: 162 | 163 | ![](./Images/ScriptingPapers-Inheritance.png) 164 | 165 | All publication-related object classes descend from an abstract base class (`library item`) which contains properties that are shared among all subclasses: creation/modification date, id and resource type. I.e., if you get all properties of a concrete object, e.g.: 166 | 167 | ```applescript 168 | tell application "Papers" to get properties of first item of keyword items 169 | ``` 170 | 171 | the superclass's properties will be returned as well: 172 | 173 | ![](./Images/ScriptingPapers-KeywordItem-Properties.png) 174 | 175 | 176 | ### AppleScript dictionary 177 | 178 | A dictionary is the part of a scriptable application that specifies the scripting terms it understands. The dictionary documents all object classes with its properties and elements, and may contain useful hints or sample code. You can choose "File > Open Dictionary" in Script Editor to display the dictionary of a scriptable application such as Papers: 179 | 180 | ![](./Images/ScriptingPapers-PapersDictionary.png) 181 | 182 | 183 | 184 | ### Container hierarchy 185 | 186 | A container is an object that contains one or more objects or properties. The application target (identified by the `tell application "Papers"` statement) constitutes the top-level container. It contains elements for all main object classes: 187 | 188 | ![](./Images/ScriptingPapers-ContainerHierarchy.png) 189 | 190 | This allows you to easily get all objects of a certain class. For instance, this would return all primary (non-supplemental) PDF files that are available in your Papers library: 191 | 192 | ```applescript 193 | tell application "Papers" 194 | get primary file items 195 | end tell 196 | ``` 197 | 198 | This is the same as above: 199 | 200 | ```applescript 201 | tell application "Papers" to get every primary file item 202 | ``` 203 | 204 | Using the `of` keyword you can walk along the container hierarchy and follow the defined object relationships. For instance, this would return a list of all publications that have a primary PDF: 205 | 206 | ```applescript 207 | tell application "Papers" 208 | get publication item of every primary file item 209 | end tell 210 | ``` 211 | 212 | Similarly, for each publication with a primary PDF, this would return a list of all authors of that publication: 213 | 214 | ```applescript 215 | tell application "Papers" 216 | get author items of publication item of every primary file item 217 | end tell 218 | ``` 219 | 220 | The result of the above command is a list of lists where the root items represent publication items and the sub-items consist of person items of each publication: 221 | 222 | ![](./Images/ScriptingPapers-PersonItem-Properties.png) 223 | 224 | 225 | ### Working with displayed & selected publications 226 | 227 | The `library window` object has properties to get the list of displayed publications, and to get or set the list of selected publications. Here's how to get all displayed publications: 228 | 229 | ```applescript 230 | tell application "Papers" 231 | set p to displayed publications of front library window 232 | end tell 233 | ``` 234 | 235 | And here's some sample code that illustrates how to get and set the list of selected publications: 236 | 237 | Get the list of publications currently selected in the displayed window mode: 238 | 239 | ```applescript 240 | tell application "Papers" 241 | set p to selected publications of front library window 242 | end tell 243 | ``` 244 | 245 | Select none: 246 | 247 | ```applescript 248 | tell application "Papers" 249 | tell front library window to set selected publications to {} 250 | end tell 251 | ``` 252 | 253 | Select all publications: 254 | 255 | ```applescript 256 | tell application "Papers" 257 | tell front library window to set selected publications to (displayed publications as list) 258 | end tell 259 | ``` 260 | 261 | Select the first displayed publication: 262 | 263 | ```applescript 264 | tell application "Papers" 265 | tell front library window to set selected publications to item 1 of (displayed publications as list) 266 | end tell 267 | ``` 268 | 269 | Select the next three publications: 270 | 271 | ```applescript 272 | tell application "Papers" 273 | tell front library window to set selected publications to items 2 thru 4 of (displayed publications as list) 274 | end tell 275 | ``` 276 | 277 | Select all publications containing a certain word in the title: 278 | 279 | ```applescript 280 | tell application "Papers" 281 | set p to every publication item whose title contains "immunotherapy" 282 | tell front library window to set selected publications to p 283 | end tell 284 | ``` 285 | 286 | Select all publications with a primary PDF: 287 | 288 | ```applescript 289 | tell application "Papers" 290 | set p to publication item of every primary file item 291 | tell front library window to set selected publications to p 292 | end tell 293 | ``` 294 | 295 | 296 | ### Changing views 297 | 298 | When querying Papers for its displayed or selected publications, the returned results depend on the current window mode and the currently selected collection. 299 | 300 | This shows how to get and set the active window mode. Available window modes: Search Mode, Library Mode, Labels Mode, Authors Mode, Sources Mode, or Reader Mode. 301 | 302 | ```applescript 303 | tell application "Papers" 304 | set m to selected mode of front library window 305 | set selected mode of front library window to Labels Mode 306 | end tell 307 | ``` 308 | 309 | And here's how to get and set the active inspector tab. Available inspector tabs: Overview Tab, Info Tab, Notes Tab, or Activity Tab. 310 | 311 | ```applescript 312 | tell application "Papers" 313 | set i to selected inspector tab of front library window 314 | set selected inspector tab of front library window to Info Tab 315 | end tell 316 | ``` 317 | 318 | 319 | ### Creating new objects 320 | 321 | The easiest way of importing existing PDFs or bibliographic metadata files is via the `open` command (see [Importing publications](#importing-publications) below). 322 | 323 | If you want to manually create a new publication, you can use the standard `make` command, e.g.: 324 | 325 | ```applescript 326 | tell application "Papers" 327 | make new publication item with properties {title:"My great new paper", publication year:"2012"} 328 | end tell 329 | ``` 330 | 331 | Here's a more detailed example for a real-world journal article: 332 | 333 | ```applescript 334 | tell application "Papers" 335 | make new publication item with properties {pmid:"21266325", pmcid:"PMC3046834", bundle volume:"60", issue:"3", page range:"735-745", publication year:"2011", publication month:"March", html title:"Deletion of <i>Lkb1</i> in Pro-Opiomelanocortin Neurons Impairs Peripheral Glucose Homeostasis in Mice", abstract:"AMP-activated protein kinase …"} 336 | end tell 337 | ``` 338 | 339 | Please note that, currently, not all available properties are writable. However, you can also create a new publication just from a DOI, and Papers will fill in all metadata and download the fulltext PDF (if it's publicly available): 340 | 341 | ```applescript 342 | tell application "Papers" 343 | set newPub to make new publication item with properties {doi:"10.1371/journal.pone.0099776"} 344 | delay 0.5 345 | set selected publications of front library window to (newPub as list) 346 | end tell 347 | ``` 348 | 349 | Note that, in the last example, the new entry must currently be selected in order to get the PDF downloaded automatically. We delay selecting the entry by 0.5 seconds so that Papers has enough time to create the new publication and display it in the interface. For an alternative way to create a publication based on a DOI, see [Importing publications](#importing-publications) below. 350 | 351 | As yet another alternative, you could also create a new publication with partial metadata (e.g., just the title) and use the `match` command to automatically complete missing metadata (see [Matching publications](#matching-publications) below). 352 | 353 | 354 | ### Updating object properties 355 | 356 | You can set new values for any writable object property. For instance, this would update the title of the first publication that's currently selected in your Papers library: 357 | 358 | ```applescript 359 | tell application "Papers" 360 | set aPub to first item of (selected publications of front library window as list) 361 | set title of aPub to "My great new paper" 362 | end tell 363 | ``` 364 | 365 | You can also edit multiple objects at once. In most cases, you'll need to specify a `whose` clause to only update objects that match a certain condition. For instance, this command batch-updates the publication year of all publications that have the keyword "test" assigned: 366 | 367 | ```applescript 368 | tell application "Papers" 369 | set publication year of every publication item whose keyword names contains "test" to 2005 370 | end tell 371 | ``` 372 | 373 | If you want to batch-update items from the list of selected (or displayed) publications, you'll currently need to use a `repeat` statement instead: 374 | 375 | ```applescript 376 | tell application "Papers" 377 | set pubList to selected publications of front library window 378 | repeat with aPub in pubList 379 | set publication year of aPub to 2014 380 | end repeat 381 | end tell 382 | ``` 383 | 384 | Please be aware that the update of objects is performed instantly and that this action cannot be undone. 385 | 386 | 387 | ### Deleting objects 388 | 389 | This will delete all currently selected publications: 390 | 391 | ```applescript 392 | tell application "Papers" 393 | delete (selected publications of front library window as list) 394 | end tell 395 | ``` 396 | 397 | As usual, you can also use a `whose` clause to specify the list of publications that a command shall act on. For instance, in the below example, the checks for `title` and `keyword names` ensure that only publications with title "My great new paper" and with no keywords assigned get deleted: 398 | 399 | ```applescript 400 | tell application "Papers" 401 | delete (every publication item whose title is "My great new paper" and keyword names is "") 402 | end tell 403 | ``` 404 | 405 | Please be aware that the deletion of objects is performed instantly and that this action cannot be undone. 406 | 407 | 408 | 409 | ## Use cases 410 | 411 | ### Importing publications 412 | 413 | Use the standard `open` command to import a bibliographic metadata file (such as a BibTeX file) or a PDF file from disk. By default, you need to specify the file's path as an HFS path which separates path components with a colon: 414 | 415 | ```applescript 416 | tell application "Papers" 417 | open "Macintosh HD:Users:msteffens:Desktop:Granskog2006.pdf" 418 | end tell 419 | ``` 420 | 421 | Alternatively, you can use a POSIX path: 422 | 423 | ```applescript 424 | tell application "Papers" 425 | open POSIX file "/Users/msteffens/Desktop/Granskog2006.pdf" 426 | end tell 427 | ``` 428 | 429 | For more info on specifying paths in AppleScript, see the [AppleScript Language Guide](https://developer.apple.com/library/mac/documentation/AppleScript/Conceptual/AppleScriptLangGuide/conceptual/ASLR_fundamentals.html#//apple_ref/doc/uid/TP40000983-CH218-SW28) and the below mentioned [AppleScript tips](http://nathangrigg.net/2012/06/some-applescript-tips/). 430 | 431 | You can also use the `open location` command to import a publication from a webpage URL or DOI: 432 | 433 | ```applescript 434 | tell application "Papers" 435 | open location "http://europepmc.org/articles/PMC3710700" 436 | end tell 437 | 438 | tell application "Papers" 439 | open location "10.1007/s00005-013-0248-8" 440 | end tell 441 | ``` 442 | 443 | 444 | ### Matching publications 445 | 446 | The Papers scripting interface features a `match` command which uses [CrossRef](http://en.wikipedia.org/wiki/CrossRef) web services to match all specified publication(s). Here's how to match all currently selected publications: 447 | 448 | ```applescript 449 | tell application "Papers" 450 | match (selected publications of front library window as list) without replacing metadata 451 | end tell 452 | ``` 453 | 454 | If `replacing metadata` is not specified or set to `false` (or prefixed with the keyword `without`), the matching process will only add missing information and not replace existing publication metadata. If you instead set `replacing metadata` to `true` (or prefix it with the keyword `with`), this will replace all existing metadata for the currently selected publications: 455 | 456 | ```applescript 457 | tell application "Papers" 458 | match (selected publications of front library window as list) with replacing metadata 459 | end tell 460 | ``` 461 | 462 | Note that this action cannot be undone, and that `with replacing metadata` will cause title formatting or additional information that has been entered for the targeted publication items to get overridden. 463 | 464 | As [mentioned above](#creating-new-objects), you can use the `match` command to complete metadata for newly created publications. In the below example, we create a new publication with just a title, and then let Papers fetch missing metadata plus the PDF via matching: 465 | 466 | ```applescript 467 | tell application "Papers" 468 | set newPub to make new publication item with properties {title:"The Estrogen Hypothesis of Obesity"} 469 | delay 0.5 470 | set selected publications of front library window to (newPub as list) 471 | set matchedPubs to match (newPub as list) without replacing metadata 472 | end tell 473 | ``` 474 | 475 | 476 | ### Exporting publications 477 | 478 | This will export all currently selected publications as BibTeX to a file (named "Bibliography.bib") on your desktop: 479 | 480 | ```applescript 481 | tell application "Papers" 482 | set outFile to ((path to desktop from user domain) as string) & "Bibliography.bib" 483 | export (selected publications of front library window as list) to outFile 484 | end tell 485 | ``` 486 | 487 | By default, publications will be exported in BibTeX format. However, you can specify the export format explicitly (e.g., RIS, EndNote XML or Papers Archive). For example, this will create a RIS file on your desktop containing all publications that have the keyword "my papers" assigned: 488 | 489 | ```applescript 490 | tell application "Papers" 491 | set outFile to ((path to desktop from user domain) as string) & "Bibliography.ris" 492 | export ((every publication item whose keyword names contains "my papers") as list) as RIS to outFile 493 | end tell 494 | ``` 495 | 496 | You can also export your annotations (i.e, notes and highlights) like this: 497 | 498 | ```applescript 499 | tell application "Papers" 500 | set outFile to ((path to desktop from user domain) as string) & "Annotations.txt" 501 | export ((every publication item) as list) as Notes to outFile 502 | end tell 503 | ``` 504 | 505 | When using the `export` command to export annotations, Papers uses your current export settings to determine the output file format (e.g., HTML, RTF, plain text, etc). 506 | 507 | As yet another example, the following will export all primary (non-supplemental) PDF files in your library to a folder (named "PDF Files") on your desktop: 508 | 509 | ```applescript 510 | tell application "Papers" 511 | set outFolder to ((path to desktop from user domain) as string) & "PDF Files" 512 | export ((publication item of every primary file item) as list) as PDF Files to outFolder 513 | end tell 514 | ``` 515 | 516 | 517 | ## Resources 518 | 519 | * [AppleScript Language Guide](https://developer.apple.com/library/mac/documentation/AppleScript/Conceptual/AppleScriptLangGuide/introduction/ASLR_intro.html) 520 | 521 | Apple's guide to the AppleScript language gives a good overview of the language and describes its lexical conventions, syntax, keywords, and other elements in detail. 522 | 523 | * [AppleScript @ Wikipedia](http://en.wikipedia.org/wiki/AppleScript) 524 | 525 | The Wikipedia page for AppleScript features also a good summary of the language essentials such as data types, conditionals, loops, handlers, etc. 526 | 527 | * [Mac OS X Automation](http://macosxautomation.com/applescript/) 528 | 529 | A website with useful AppleScript resources ranging from tutorials (such as a [step-by-step AppleScript tutorial](http://macosxautomation.com/applescript/firsttutorial/index.html)), [script examples](http://macosxautomation.com/applescript/learn.html) and links to further [online resources about AppleScript](http://macosxautomation.com/applescript/resources.html). 530 | 531 | * [AppleScript for Absolute Starters](http://fischer-bayern.de/as/as4as/AS4AS_e.pdf) 532 | 533 | A free eBook by Bert Altenburg published in 2003 that is still a useful introduction to AppleScript. 534 | 535 | * [Some AppleScript tips](http://nathangrigg.net/2012/06/some-applescript-tips/) 536 | 537 | A good compilation of AppleScript tips by Nathan Grigg that explain, for instance, how to specify files in AppleScript, how to run an AppleScript from the command line, or how to pass variables by reference. 538 | 539 | * Since OS X 10.6, AppleScript allows to call Cocoa methods from within AppleScript scripts. The aforementioned [Mac OS X Automation](http://macosxautomation.com/applescript/) web site has a good compilation of [resources and tools for AppleScriptObjC](http://macosxautomation.com/applescript/apps/index.html). 540 | 541 | 542 | ## Tools 543 | 544 | * [Script Debugger](http://www.latenightsw.com) is a powerful third-party AppleScript debugger. Besides full debugging capabilities, Script Debugger features advanced dictionary viewers. It also offers unique value explorers that allow you to dynamically inspect the objects and properties offered by a scriptable application. Examples of its dictionary viewers and value explorers can be seen in some of the screenshots above. 545 | 546 | -------------------------------------------------------------------------------- /docs/Papers3/Images/ScriptingPapers-ContainerHierarchy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/extracts/mac-scripting/07649f2dadc592a68048ec88c79e31e6037eefac/docs/Papers3/Images/ScriptingPapers-ContainerHierarchy.png -------------------------------------------------------------------------------- /docs/Papers3/Images/ScriptingPapers-Inheritance.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/extracts/mac-scripting/07649f2dadc592a68048ec88c79e31e6037eefac/docs/Papers3/Images/ScriptingPapers-Inheritance.png -------------------------------------------------------------------------------- /docs/Papers3/Images/ScriptingPapers-KeywordItem-Properties.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/extracts/mac-scripting/07649f2dadc592a68048ec88c79e31e6037eefac/docs/Papers3/Images/ScriptingPapers-KeywordItem-Properties.png -------------------------------------------------------------------------------- /docs/Papers3/Images/ScriptingPapers-LibraryWindow-Properties.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/extracts/mac-scripting/07649f2dadc592a68048ec88c79e31e6037eefac/docs/Papers3/Images/ScriptingPapers-LibraryWindow-Properties.png -------------------------------------------------------------------------------- /docs/Papers3/Images/ScriptingPapers-PapersDictionary.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/extracts/mac-scripting/07649f2dadc592a68048ec88c79e31e6037eefac/docs/Papers3/Images/ScriptingPapers-PapersDictionary.png -------------------------------------------------------------------------------- /docs/Papers3/Images/ScriptingPapers-PersonItem-Properties.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/extracts/mac-scripting/07649f2dadc592a68048ec88c79e31e6037eefac/docs/Papers3/Images/ScriptingPapers-PersonItem-Properties.png -------------------------------------------------------------------------------- /docs/Papers3/Images/ScriptingPapers-PublicationItem-Elements.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/extracts/mac-scripting/07649f2dadc592a68048ec88c79e31e6037eefac/docs/Papers3/Images/ScriptingPapers-PublicationItem-Elements.png -------------------------------------------------------------------------------- /docs/Papers3/Images/ScriptingPapers-PublicationItem-Properties.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/extracts/mac-scripting/07649f2dadc592a68048ec88c79e31e6037eefac/docs/Papers3/Images/ScriptingPapers-PublicationItem-Properties.png -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman --------------------------------------------------------------------------------