├── LICENSE ├── Notebook Export Manager.applescript ├── README.md ├── Test_Top.png ├── Test_notebook.png └── Test_tags.png /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 BitRancher 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 | -------------------------------------------------------------------------------- /Notebook Export Manager.applescript: -------------------------------------------------------------------------------- 1 | -- 2 | -- Notebook Export Manager: An Applescript to export data from Evernote 3 | -- 4 | -- Copyright (c) 2021 William C Jacob Jr 5 | -- Licensed under the MIT license. See accompanying documentation. 6 | -- 7 | -- This works with the version of evernote installed from their website, 8 | -- but not with the Apple Mac App Store version 10 which doesn't 9 | -- support AppleScript at this time (Feb 2021). 10 | -- Runs successfully on macOS Catalina (10.15.7) and Big Sur (11.1) with Evernote 7.14 11 | -- 0.4.2 fix locale problem caused by date in string literal 12 | -- 0.5.0 fix file Kind search for macOS Monterery (12.0.1): HTML files on disk now 13 | -- show file Kind "HTML document", not "HTML text" 14 | 15 | -- To get Finder write 16 | use scripting additions 17 | -- To get NSString access in urlEncode handler 18 | -- use framework "Foundation" removed in 0.3.2 19 | 20 | -- 21 | -- Users may wish to change these initial values 22 | -- 23 | global logProgress -- switch to control Log statements for debugging 24 | set logProgress to false -- run Log commands for debugging USER SETTING 25 | global alwaysBuildTopIndex 26 | set alwaysBuildTopIndex to true -- force rebuild of top-level index USER SETTING 27 | -- The following setting might be useful if you make changes to the buildNoteIndex handler 28 | -- and you want to rebuild the index pages for each notebook without actually exporting 29 | -- all the notes from Evernote again. 30 | global alwaysBuildBookIndexes 31 | set alwaysBuildBookIndexes to false -- force rebuild of notebook indexes USER SETTING 32 | global myPListFullPath -- PList full path + file name 33 | -- The Property List file for this application (USER SETTING): 34 | -- Store it in [my home folder]/Library/Preferences: 35 | set myPListFullPath to POSIX path of ((path to preferences as text) & "com.lake91.nem.plist") -- Folder for preferences 36 | -- Or, store it in Downloads, just for testing (comment this out to use standard location) 37 | -- set myPListFullPath to POSIX path of ((path to downloads folder as text) & "com.lake91.enexp.plist") -- Test folder 38 | 39 | -- Global variables 40 | -- 41 | global parentTagNames, parentTagHTML -- cache of tag names 42 | set parentTagNames to {} -- list of tag names already looked up 43 | set parentTagHTML to {} -- corresponding list of parent tag HTML 44 | 45 | global offsetGMT -- offset to GMT in seconds 46 | set offsetGMT to time to GMT -- get offset to GMT in seconds 47 | global headerInfo -- system info to store as comment in index pages 48 | set headerInfo to "" -- initially empty 49 | global plExportTypeDesc -- Descriptions for options 50 | global myCSSfile, myJSfile, myTagFile, myTopIndex -- file names for top-level folder 51 | global nbIndexFile -- filename for notebook index pages 52 | global FolderHTML, FolderENEX, AllNotebooks, SearchString -- Properties 53 | global ExportAsHTML, ExportAsENEX, TimeoutMinutes, BooksSelected, ExportDate -- Properties 54 | global frontProcess -- name of our process 55 | set myCSSfile to "enstyles.css" -- CSS file name for html export folder 56 | set myJSfile to "enscripts.js" -- JS file name for html export folder 57 | set myTagFile to "entags.html" -- master tag list file for html export folder 58 | set myTopIndex to "index.html" -- name of the top-level index page listing all notebooks 59 | set nbIndexFile to "index.html" -- name of the notebook index page listing all notes 60 | 61 | -- For internal timing 62 | global timerAll, timerSub1 63 | set timerAll to 0 64 | set timerSub1 to 0 65 | 66 | if logProgress then log "Property List file: " & myPListFullPath 67 | 68 | set plExportTypeDesc to {"Export only in HTML format", "Export only in ENEX format", "Export in both HTML and ENEX formats"} 69 | 70 | -- Set initial defaults for properties 71 | set FolderHTML to path to documents folder as text -- Path for HTML exports 72 | set FolderENEX to path to documents folder as text -- Path for ENEX exports 73 | set AllNotebooks to true -- export all notebooks, not just some 74 | set SearchString to "" -- assume no user-supplied search string 75 | set ExportAsHTML to true -- Export notebooks in HTML format 76 | set ExportAsENEX to true -- Export notebooks in ENEX format 77 | set BooksSelected to {} -- List of notebooks to export 78 | set ExportDate to initDate(2000, 1, 1) -- No exports have been done yet 0.4.2 79 | set TimeoutMinutes to 30 -- Default timeout 80 | 81 | -- Wake up Evernote and then switch back to this script 82 | tell application "System Events" to set frontProcess to the name of every process whose frontmost is true -- get our process name 83 | if logProgress then log "frontProcess: " & frontProcess 84 | if application "Evernote" is not running then 85 | activate application "Evernote" -- start evernote if not already running 86 | delay 5 -- give it a few seconds to crank up 87 | activate frontProcess -- Switch back to our app so user sees the prompts 88 | delay 2 89 | end if 90 | -- 91 | -- Get Properties from PList file 92 | -- Create Piist if necessary and Get current values from Property List file 93 | -- 94 | set plistPrompt to not managePlist(1) -- Get PList values (returns false if the PList file did not exist) 95 | activate frontProcess -- Switch back to our app so user sees the prompts 96 | delay 1 97 | -- 98 | -- Loop to prompt the user to start the export or to adjust the settings 99 | -- 100 | repeat -- Until user clicks Export Now or Cancel 101 | if plistPrompt then -- If PList is new or it's a second pass through this loop 102 | -- Show Properties and allow user to make changes 103 | if not managePlist(2) then return -- update PList; quit if user cancels 104 | activate frontProcess -- Switch back to our app so user sees the prompts 105 | delay 1 -- give it a second 106 | end if 107 | set plistPrompt to true -- If we loop in the repeat, let user update PList 108 | set msg to "Will consider " & cond(AllNotebooks, "all notebooks", ("only " & (count of BooksSelected) & " selected notebooks")) & " for export to 109 | HTML: " & cond(ExportAsHTML, POSIX path of FolderHTML, "(not to be exported)") & " 110 | ENEX: " & cond(ExportAsENEX, POSIX path of FolderENEX, "(not to be exported)") & " 111 | " & cond(length of SearchString > 0, "Only notes meeting this criteria will be exported: 112 | " & SearchString, "All notes will be exported in each notebook") & " 113 | 114 | Start export now or change settings." 115 | if ("Export Now" = (button returned of (display dialog msg buttons {"Export now", "Change settings", "Cancel"} default button 1 cancel button 3 with title "Ready to Export" with icon note))) then exit repeat 116 | end repeat 117 | 118 | set timerAll to getFineTime() -- get time at start ot run 119 | -- 120 | -- Build a list of notebooks to consider, either all of them or a selected list 121 | -- 122 | set exportBooks to BooksSelected -- assume a user-selected list 123 | if AllNotebooks then set exportBooks to getBooks() -- get sorted list of all notebooks 124 | -- 125 | -- Check that the volumes containing the output folders are mounted (eg, if on a USB stick) 126 | -- 127 | if ExportAsENEX then 128 | if not checkRunVol(FolderENEX, "ENEX") then return -- check volume and folder; quit if not found 129 | end if 130 | if ExportAsHTML then 131 | if not checkRunVol(FolderHTML, "HTML") then return -- check volume and folder; quit if not found 132 | end if 133 | -- 134 | -- Set up counters for the main processing loop 135 | -- 136 | with timeout of (TimeoutMinutes * 60) seconds -- run this code with a timeout 137 | set countENEXtried to 0 -- init count of attempted ENEX exports 138 | set countHTMLtried to 0 -- init count of attempted HTML exports 139 | set countENEXdone to 0 -- init count of completed ENEX exports 140 | set countHTMLdone to 0 -- init count of completed HTML exports 141 | set countBooksAttempted to length of exportBooks -- count of notebooks being attempted 142 | set ExportDate to current date -- Set date of main index rebuild 143 | set progress total steps to (length of exportBooks) -- set up progress display 144 | set progress completed steps to 0 145 | set progress description to "Exporting notebooks" 146 | set pcounter to 0 147 | -- 148 | -- This is the main loop for this script 149 | -- Export each eligible notebook in sequence 150 | -- 151 | repeat with aBook in exportBooks -- repeat with each notebook name 152 | if ExportAsENEX then -- if ENEX export was requested 153 | set progress additional description to "Exporting " & aBook & " as ENEX" -- set progress display detail 154 | set countENEXtried to countENEXtried + 1 -- count attempts 155 | if exportNotes(aBook, (POSIX path of FolderENEX) & my repl(aBook, "/", "_") & ".enex", "ENEX") then set countENEXdone to countENEXdone + 1 -- export as ENEX anc count successes 156 | end if 157 | if ExportAsHTML then -- if HTML export was requested 158 | set progress additional description to "Exporting " & aBook & " as HTML" -- set progress display detail 159 | set countHTMLtried to countHTMLtried + 1 -- count attempts 160 | set outHTML to (POSIX path of FolderHTML) & my repl(aBook, "/", "_") & "/" -- build HTML export folder path 161 | set expSome to exportNotes(aBook, outHTML, "HTML") -- Try to export notebook 162 | if expSome then set countHTMLdone to countHTMLdone + 1 -- count successful exports 163 | if expSome or alwaysBuildBookIndexes then buildNoteIndex(aBook, outHTML, expSome) -- build the index.html file for this notebook 164 | end if 165 | set pcounter to pcounter + 1 -- increment the progress display counter 166 | set progress completed steps to pcounter -- and ask Applescript to display it 167 | end repeat 168 | -- 169 | -- Build the top-level index.html page if anything was exported 170 | -- or if it doesn't exist or if it is to be built every time. 171 | -- 172 | tell application "Finder" to set gotIndex to (exists (FolderHTML & myTopIndex)) -- does top index exist ? 173 | if (countHTMLdone > 0) or (not gotIndex) or alwaysBuildTopIndex then buildTopIndex() -- rebuild top index if missing or outdated 174 | if (countENEXdone > 0) or (countHTMLdone > 0) then set x to managePlist(3) -- Update the Property List file with new Export Date 175 | set progress total steps to 0 -- shut down the progress display 176 | set progress completed steps to 0 177 | set progress description to "" 178 | set progress additional description to "" 179 | 180 | set timerAll to getFineTime() - timerAll -- elapsed seconds for run 181 | -- display alert "Run Time: " & timerAll & " Sub Time: " & timerSub1 182 | -- 183 | -- Display results and exit 184 | -- 185 | display alert cond((countENEXdone + countHTMLdone) > 0, "Some notebooks exported", "No notebooks exported") message ((countBooksAttempted as text) & " Notebooks considered 186 | " & countHTMLdone & " of " & countHTMLtried & " exported as HTML and 187 | " & countENEXdone & " of " & countENEXtried & " exported as ENEX 188 | 189 | (Export time (secs): " & (timerAll as integer) & ", index read: " & (timerSub1 as integer) & ")") 190 | end timeout 191 | return -- end execution of this script 192 | 193 | -- 194 | -- Select notes and export them 195 | -- bName is the notebook name (text) 196 | -- oFIle is the full pathname of the output file (ENEX) or folder (HTML) 197 | -- oType is either "ENEX" or "HTML" 198 | -- Returns True if some were exported; else False 199 | -- A lot of logic in this handler is devoted to handling notebooks whose 200 | -- name contains a quote ("). 201 | -- 202 | on exportNotes(bName, oFile, oType) 203 | local f, fileExists, datesrch, enexp, srch, someNotes 204 | local selectedNotes, d, cu, b, c, i, m, w, j 205 | if logProgress then log "Exporting notebook: " & bName & " to file: " & oFile & " as " & oType 206 | tell application "Evernote" to set f to exists notebook bName -- does notebook exist? 207 | if not f then return false -- if not, stop now and return 208 | -- set QuoteInName to cond((offset of "\"" in bName) > 0, true, false) -- notebook name contains " 209 | tell application "Finder" to set fileExists to (exists oFile as POSIX file) -- check for output file existence 210 | set datesrch to "" -- Assume this notebook has never been exported 211 | set enexp to "" -- assume no export-date found in existing exported notebook 212 | if fileExists then -- if output file already exists 213 | -- if oType = "ENEX" then tell application "System Events" to set enexp to value of XML attribute "export-date" of XML element "en-export" of XML file oFile -- Get export date/time from inside ENEX file (this was very slow for large ENEX files and was replaced in ver 0.3.2) 214 | if oType = "ENEX" then set enexp to item 1 of readIndex(POSIX file oFile, {""}, 500) -- get export from HTML comment 216 | if (length of enexp) > 0 then set datesrch to " updated:" & enexp -- Set export date clause for search string 217 | end if 218 | if (offset of "\"" in bName) = 0 then -- if normal notebook name (no " in it) 219 | set srch to "notebook:\"" & bName & "\" " & SearchString -- Build basic search string for EXPORT 220 | tell application "Evernote" to set someNotes to find notes srch & datesrch -- any updated notes? 221 | if logProgress then log oType & " note count for: " & bName & " is: " & (count of someNotes) & " with search: " & srch & datesrch 222 | if (count of someNotes) = 0 then return false -- If no new notes, don't export this notebook 223 | tell application "Evernote" to set selectedNotes to find notes srch -- get list of notes to export 224 | else -- there is a " in the notebook name - no search allowed 225 | -- tell application "Evernote" to set selectedNotes to every note in notebook bName -- export all notes 226 | if enexp ≠ "" then -- if only updated notes 227 | set d to current date -- init date object 228 | tell d to set {its year, its month, its day, its time} to {0 + (text 1 thru 4 of enexp), 0 + (text 5 thru 6 of enexp), 0 + (text 7 thru 8 of enexp), (hours * (0 + (text 10 thru 11 of enexp))) + (minutes * (0 + (text 12 thru 13 of enexp))) + (0 + (text 14 thru 15 of enexp))} -- convert date string into type date 229 | set d to d + offsetGMT -- adjust GMT export-date to local time 230 | if logProgress then log "enexp: " & enexp & " local: " & d 231 | tell application "Evernote" to set cu to count of (every note in notebook bName whose modification date ≥ d) -- get count of updated notes in this notebook 232 | if cu = 0 then return false -- if no changed notes in notebook, don't export it 233 | end if 234 | tell application "Evernote" 235 | if SearchString = "" then -- did user specify a search string? 236 | set selectedNotes to every note in notebook bName -- if not, export every note in notebook 237 | else 238 | set b to find notes SearchString -- search all of evernote with user-specified search 239 | set selectedNotes to {} -- init list of notes to export 240 | set c to every note in notebook bName -- get all notes in notebook 241 | repeat with i from 1 to (length of c) -- with every note in notebook 242 | set m to item i of c -- get the note from the notebook 243 | repeat with j from 1 to length of b -- with every note in search str results 244 | set w to item j of b -- get note from search results 245 | if m is w then -- if note is in both lists 246 | copy m to end of selectedNotes -- copy it to the result list 247 | exit repeat 248 | end if 249 | end repeat 250 | end repeat 251 | end if 252 | end tell 253 | if logProgress then log oType & " note count for: " & bName & " is: " & (count of selectedNotes) & " with quoted notebook name" 254 | end if 255 | if (count of selectedNotes) = 0 then return false -- If no notes to export, return empty list 256 | if oType = "ENEX" then tell application "Evernote" to export selectedNotes to oFile format ENEX with tags -- export notes as enex 257 | if oType = "HTML" then tell application "Evernote" to export selectedNotes to oFile format HTML -- or as HTML 258 | return true -- return: aome notes exported 259 | end exportNotes 260 | 261 | -- 262 | -- Build the index.html file for a notebook, replacing the simple 263 | -- one produced by the evernote Export as HTML operation 264 | -- Input: Notebook name (string), Output folder name (string), 265 | -- Did export just occur (boolean) 266 | -- Returns nothing 267 | -- 268 | on buildNoteIndex(bookName, outHTML, wasExported) 269 | -- 270 | -- Initialize variables before processing each note in the notebook 271 | -- 272 | local bookMod, bookCreate, htmlNotes, tagRows, yy, noteBookCell, pfile, listNoteFiles 273 | local noteAttrs, noteHTMLfile, sNoteTitle, fileEnc, notelink, noteLinkFar 274 | local aNoteMod, aNoteCreate, tagsComma, allTagNames, aNoteTagList 275 | local aTageName, seenTag, parentCell, loopTag, spanTag, sNoteAttach, dirSize 276 | local aNoteAuthor, aNoteExporter, fOff, lOff, sz, expStamp, priorEnviron, priorExportDate 277 | local ourTimer, bookNotes, bookFiles 278 | -- tell application "Evernote" to set bookType to the notebook type of notebook bookName -- get type of notebook 279 | set bookMod to initDate(2000, 1, 1) -- default book mod date 0.4.2 280 | set bookCreate to initDate(2040, 1, 1) -- default book create date 0.4.2 281 | set htmlNotes to "" -- Building HTML for Notes list 282 | set tagRows to "" -- Building HTML for rows with Tag info 283 | set bookNotes to 0 -- init count of notes in this notebook 284 | set bookFiles to 0 -- init count of attachments 285 | set yy to my urlEncode(my repl(bookName, "/", "_")) & "/" & nbIndexFile -- index file for this notebook 286 | set yy to my repl(yy, ":", "%3A") -- handle filename with : 287 | set noteBookCell to " " & my textHT(bookName) & " 288 | " 289 | -- Get a list of all .html files in the exported folder for this notebook, except for index.html 290 | try 291 | -- tell application "Finder" to set listNoteFiles to every item of (POSIX file outHTML as alias) whose ((kind is "HTML text") and (name ≠ nbIndexFile)) -- comment 0.3.5 292 | set yy to alias (outHTML as POSIX file) 293 | tell application "System Events" to set listNoteFiles to ((every file of yy) whose (((kind is "HTML text") or (kind is "HTML document")) and (name ≠ nbIndexFile))) -- get every note html file 0.5.0 294 | on error 295 | return -- if no files found, return without building the notebook index 296 | end try 297 | if (count of listNoteFiles) = 0 then return -- if no files, return 0.3.5 298 | -- 299 | -- If this notebook was just exported, use today's date as the export date. If not 300 | -- get the actual export date from the existing index.html file. 301 | -- 302 | set expStamp to current date -- assume this notebook was actually just exported 303 | set priorEnviron to "" -- assume no prior "environment" comment exists 304 | set priorExportDate to "" -- assume no prior export date 305 | if not wasExported then -- if it wasn't just exported ... 306 | set yy to readIndex(POSIX file (outHTML & nbIndexFile), {"", " -->"}, 500) -- Get export date and environment comment from old index.html 307 | if length of (item 1 of yy) > 0 then 308 | set priorExportDate to item 1 of yy -- remember export date as string 309 | set expStamp to getDateSrch(item 1 of yy) -- get export date from previous index file 310 | set priorEnviron to item 2 of yy -- remember previous environment comment 311 | end if 312 | end if 313 | -- 314 | -- Read the first few hundred characters of every exported note file (html) to get metadata 315 | -- for that note and create appropriate table rows for the index file. 316 | -- 317 | repeat with noteHTMLfile in listNoteFiles -- examine each note in the current notebook 318 | -- set noteAttrs to getMeta(noteHTMLfile as text) -- get Meta tags from the note html file 0.3.5 319 | set noteAttrs to getMeta2(noteHTMLfile) -- get Meta tags from the note html file 0.3.5 320 | set aNoteTitle to nname of noteAttrs -- get note title from meta data 321 | set aNoteAuthor to author of noteAttrs -- get author from meta data 322 | set fOff to offset of "<" in aNoteAuthor -- get position of email start char 323 | set lOff to offset of ">" in aNoteAuthor -- and end char 324 | if (fOff > 2) and ((offset of "@" in aNoteAuthor) > fOff) and (lOff > (offset of "@" in aNoteAuthor)) then -- if Author field contains something plus an email address 325 | set aNoteAuthor to " " & textHT(trim(text 1 thru (fOff - 1) of aNoteAuthor)) & " ✉️ 326 | " 327 | else 328 | set aNoteAuthor to " " & cond(aNoteAuthor = "", " ", textHT(aNoteAuthor)) & " 329 | " 330 | end if 331 | set aNoteExporter to exporter of noteAttrs -- evernote version 332 | set fileEnc to my urlEncode(name of noteHTMLfile) -- get current note file name (no path) 333 | set fileEnc to my repl(fileEnc, "/", "%3A") -- handle filename with : which evernote saves as / 334 | -- Build table cell containing link to Note html file (one for note index and one for top-level tag index) 335 | set notelink to " " & my textHT(aNoteTitle) & " 336 | " 337 | set noteLinkFar to " " & my textHT(aNoteTitle) & " 338 | " 339 | set aNoteMod to getDateMeta(updated of noteAttrs) -- get note update date (as class date) 340 | set aNoteCreate to getDateMeta(created of noteAttrs) -- get note create date (as class date) 341 | if aNoteMod > bookMod then set bookMod to aNoteMod -- track most recently changed note in notebook 342 | if aNoteCreate < bookCreate then set bookCreate to aNoteCreate -- and earliest created note in notebook 343 | set tagsComma to (tags of noteAttrs) as text -- comma separated list of tags from note html meta tags 344 | if tagsComma = "" then -- if no tags 345 | set allTagNames to " " -- placeholder for table cell 346 | else -- process the tags assigned to this note 347 | -- 348 | -- Separate the string of tag names into a list of tags. Then 349 | -- figure out if each tag has a parent tag. Keep a cache of tags 350 | -- we've already checked so we don't have to ask Evernote again and again. 351 | -- Create table cells with the tag and with the parent tags (if any) 352 | -- 353 | set AppleScript's text item delimiters to ", " 354 | set aNoteTagList to every text item of tagsComma 355 | set AppleScript's text item delimiters to "" 356 | set allTagNames to "" -- initialize a string of tag names for the note detail line 357 | repeat with aTagName in aNoteTagList -- process each tag 358 | -- set aTagName to name of aTag -- name of current tag 359 | set seenTag to my item_position({aTagName}, parentTagNames) -- seen tag before? 360 | if seenTag = 0 then -- have not seen this tag before 361 | set parentCell to "" -- init hierarchy of tag names 362 | -- loop up through linked parent tags (if any) 363 | tell application "Evernote" 364 | set loopTag to tag aTagName 365 | repeat while (parent of loopTag) is not missing value 366 | set parentCell to my fmtTag(name of loopTag) & parentCell -- pre-pend parent tag 367 | set loopTag to parent of loopTag 368 | end repeat 369 | set parentCell to my fmtTag(name of loopTag) & parentCell -- pre-pend parent tag 370 | end tell 371 | copy parentCell to end of parentTagHTML -- cache this parent tag info 372 | copy (aTagName) to end of parentTagNames -- and this tag 373 | else -- get tag parent string from previously cached list 374 | set parentCell to (item seenTag of parentTagHTML) -- get parent hierarchy data 375 | end if 376 | set spanTag to my fmtTag(aTagName) -- format the Tag Name 377 | set allTagNames to allTagNames & spanTag -- add to cell for tag table detail row 378 | -- Construct an HTML table row for the tag table, which will appear in 379 | -- the bottom half of the notebook index page and in the top-level 380 | -- all-tags page. 381 | set tagRows to tagRows & " 382 | " & spanTag & " 383 | " & notelink & noteLinkFar & " " & parentCell & " 384 | " & noteBookCell & " 385 | " 386 | end repeat 387 | end if 388 | -- 389 | -- Figure out file sizes and how many attachments this note has 390 | -- 391 | set sz to size of (get info for noteHTMLfile) -- size of note html file 0.3.5 392 | set aNoteAttach to {} -- assume no attachments for this note 393 | try -- handle error if no resources folder exists 394 | tell application "System Events" -- 0.3.5 395 | set resourceFolder to (POSIX path of noteHTMLfile) & ".resources" 396 | set sz to sz + (size of folder resourceFolder) -- add size of resources folder 397 | set aNoteAttach to every file of folder resourceFolder -- get list of attachments 398 | end tell 399 | end try 400 | set aNoteAttach to count of aNoteAttach -- count attachments 401 | set bookFiles to bookFiles + aNoteAttach -- count attachments in this notebook 402 | set bookNotes to bookNotes + 1 -- count notes in this notebook 403 | set htmlNotes to htmlNotes & " 404 | " & notelink & my dateCell(aNoteCreate) & my dateCell(aNoteMod) & fmtSize(sz) & " " & aNoteAttach & " 405 | " & aNoteAuthor & " " & allTagNames & " 406 | " 407 | end repeat -- End of processing for notes in current notebook 408 | -- 409 | -- All notes in this notebook have been processed. Get ready to write the index page 410 | -- for the notebook. 411 | -- 412 | set yy to alias (outHTML as POSIX file) 413 | tell application "System Events" to set dirSize to size of yy -- get size of notebook 0.3.5 414 | -- set dirSize to my getSize({outHTML}) -- size of this html notebook folder in text and as a number 0.3.5 415 | -- Build the table row for this notebook. It will be imbedded as an HTML comment in 416 | -- the note index file and, later, extracted to build the top-level index page 417 | set yy to count of listNoteFiles -- count of notes in this notebook 418 | set noteBookRow to " 419 | " & noteBookCell & " " & yy & " 420 | " & dateCell(bookCreate) & dateCell(bookMod) & dateCell(expStamp) & fmtSize(dirSize) & " " & bookFiles & " 421 | 422 | " 423 | set sortTags to "" -- assume no tag table to sort (fragment for the body onload attribute) 424 | -- if there are tags in this notebook, construct the table to show them in 425 | -- the notebook index page 426 | if (length of tagRows) > 0 then 427 | set tagRows to "
Tags in this Notebook
428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | " & tagRows & " 441 | 442 |
Tag NameNoteNoteParentsNotebook
443 | " 444 | set sortTags to "hideNBnames('nbname'); sortTable('tagTable',0,0);" -- for onload attribute 445 | end if 446 | set {year:y1, month:m1, day:d1, hours:h1, minutes:r1, seconds:s1} to ((expStamp - offsetGMT) as date) 447 | set ENdate to (y1 as text) & lzer(m1 as number) & lzer(d1 as text) & "T" & lzer(h1 as text) & lzer(r1 as text) & lzer(s1 as text) & "Z" -- build export date to embed as comment in notebook index page 448 | if priorExportDate ≠ "" then set ENdate to priorExportDate -- use prior export date if book wasn't just exported 449 | -- 450 | -- Build the notebook index page for this notebook 451 | -- 452 | set htmlNotes to " 453 | 454 | 455 | " & getSysInfo(priorEnviron) & " 457 | 458 | " & bookName & " 459 | 460 | 461 | 462 | 463 | " & pageHead("Notebook: " & my textHT(bookName), "All Notebooks  All Tags", "Click headings to sort. This notebook was exported from Evernote " & my fmtLongDate(expStamp) & " " & tzone() & "
Totals: " & bookNotes & " notes, " & makeSize(dirSize) & " size, " & bookFiles & " files (attachments)", true) & " 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | " & htmlNotes & " 478 |
Note TitleCreatedModifiedSizeFilesAuthorTags
479 | " & tagRows & " 480 |

481 | 482 | 483 | " 484 | my writeIndex(outHTML & nbIndexFile, htmlNotes) -- write the index.html for this notebook 485 | return 486 | end buildNoteIndex 487 | -- 488 | -- Build top level index.html page by reading data stored as comments in 489 | -- each of the notebook index pages 490 | -- 491 | on buildTopIndex() 492 | local bookHTML, tagHTML, buildDate, folderList, fol, pf 493 | local bookdata, pcounter, htmlBooks, tagOutput, exi 494 | local totNotes, totSize, totFiles, totBooks 495 | -- get names of all subfolders 496 | -- tell application "Finder" to set folderList to every item of alias FolderHTML whose kind is "Folder" 497 | tell application "System Events" to set folderList to every folder of alias FolderHTML -- 0.3.5 498 | if logProgress then log "Folders being indexed" 499 | set bookHTML to "" -- init table body html for list of notebooks 500 | set tagHTML to "" -- init table body html for list of tags 501 | set buildDate to current date -- date & time these index pages are built 502 | set totBooks to count of folderList 503 | set totNotes to 0 -- init totals 504 | set totSize to 0 505 | set totFiles to 0 506 | set progress total steps to totBooks -- set up progress display 507 | set progress completed steps to 0 -- set progress counter to zero 508 | set progress description to "Building top-level index" 509 | set pcounter to 0 -- counter for progress display 510 | -- 511 | -- This loop reads every notebook index page and extracts 512 | -- data elements that were stored as comments in the html 513 | -- 514 | repeat with fol in folderList -- for every subfolder 515 | tell application "System Events" to set pf to name of fol -- 0.3.5 516 | set progress additional description to "Processing " & pf 517 | -- Read the index.html file from the subfolder and look for two strings: 518 | -- the table entry for the notebook itself in the top-level index page 519 | -- the list of tags for this notebook, if any 520 | -- we look for both in this one operation so that we don't have to 521 | -- read the file two times. 522 | -- tell application "System Events" to set pf to POSIX path of fol 523 | tell application "System Events" to set pf to ((path of fol) & nbIndexFile) 524 | set exi to true 525 | try 526 | set pf to alias pf 527 | on error 528 | set exi to false 529 | end try 530 | if exi then -- if index.html exists in this folder 531 | set bookdata to readIndex(pf, {" 533 | ", "data-notes='", "data-foldersize='", "data-files='"}, {"bookindex -->", "", "'", "'", "'"}, 0) -- read the index.html page in the subfolder 534 | set bookHTML to bookHTML & " " & item 1 of bookdata -- keep the link data 535 | set tagHTML to tagHTML & item 2 of bookdata -- and the tag data 536 | try 537 | set totNotes to totNotes + (0 + (item 3 of bookdata)) 538 | set totSize to totSize + (0 + (item 4 of bookdata)) 539 | set totFiles to totFiles + (0 + (item 5 of bookdata)) 540 | end try 541 | set pcounter to pcounter + 1 542 | set progress completed steps to pcounter 543 | end if 544 | end repeat -- all notebook index pages have now been read 545 | -- Build the HTML for the top-level index.html page 546 | set htmlBooks to " 547 | 548 | " & getSysInfo("") & " 549 | All Notebooks 550 | 551 | 552 | 553 | 554 | " & pageHead("All Exported Notebooks", "All Tags", "Click headings to sort. This page was built " & fmtLongDate(buildDate) & " " & tzone() & ".
Totals: " & totBooks & " notebooks, " & totNotes & " notes, " & makeSize(totSize) & " size, " & totFiles & " files (attachments)", true) & " 555 | 556 | 557 | 558 | 559 | 560 | 561 | 562 | 563 | 564 | 565 | 566 | 567 | 568 | " & bookHTML & " 569 |
Notebook NameNotesCreatedModifiedExportedSizeFiles
570 |

571 | 572 | 573 | " 574 | my writeIndex((POSIX path of FolderHTML) & myTopIndex, htmlBooks) -- write top-level index page for all notebooks 575 | -- build placeholder HTML for the top-level list of tags in case there are none 576 | set tagOutput to " 577 | 578 | 579 | All Tags 580 | 581 | 582 | 583 | 584 | " & pageHead("All Tags", "All Notebooks", "No tags were found in any of the exported notebooks.", false) & " 585 |
 
586 |

587 | 588 | 589 | " 590 | -- if there are tags, build HTML for the All Tags page 591 | if length of tagHTML > 0 then set tagOutput to " 592 | 593 | 594 | All Tags 595 | 596 | 597 | 598 | 599 | " & pageHead("All Tags", "All Notebooks", "Click headings to sort. This page was built " & fmtLongDate(buildDate) & " " & tzone() & ".", true) & " 600 | 601 | 602 | 603 | 604 | 605 | 606 | 607 | 608 | 609 | 610 | 611 | " & tagHTML & " 612 | 613 |
Tag NameNoteNoteParentsNotebook
614 |

615 | 616 | 617 | " 618 | my writeIndex((POSIX path of FolderHTML) & myTagFile, tagOutput) -- write all-tag page 619 | my writeJS() -- write javascript file if it's not already there 620 | my writeCSS() -- write CSS file if it's not already there 621 | end buildTopIndex 622 | -- 623 | -- Extract meta tag info from HTML note file 624 | -- reads a hfs file name, expecting it to be an HTML note file 625 | -- returns note metadata as a Record 626 | -- 627 | on getMeta(ifile) 628 | local resp, XMLAppData, t, nm, cn, metas 629 | set ourTimer to getFineTime() -- init sub timer 630 | -- Read first 2000 bytes of the note html file, hoping to get the entire 631 | -- ... group. Append those tags to the returned string so 632 | -- the system event XML processor will see them. 633 | set hd to "" & (item 1 of readIndex(ifile, {""}, {""}, 2000)) & "" 634 | set resp to {nname:"", author:"", created:"", tags:{""}, updated:"", exporter:""} -- init returned value 635 | tell application "System Events" 636 | -- set XMLAppData to make new XML data with properties {name:"htData", text:hd} 637 | -- tell XML data "htData" -- id XMLAppData 638 | tell (make new XML data with properties {text:hd}) -- pass the note html to the xml scanner 639 | tell XML element "head" -- look within the head tag 640 | set metas to every XML element whose name = "meta" -- get all meta tags 641 | repeat with t from 1 to (length of metas) -- look at each meta tag in turn 642 | set thismeta to item t of metas -- get this meta tag 643 | if exists (XML attribute "name" of thismeta) then -- if there'a a 'name' attribute 644 | set nm to value of XML attribute "name" of thismeta -- get the name attribute 645 | set cn to value of XML attribute "content" of thismeta -- and the content attribute 646 | if nm is "exporter-version" then set exporter of resp to cn -- extract based on name .... 647 | if nm is "created" then set created of resp to cn 648 | if nm is "updated" then set updated of resp to cn 649 | if nm is "author" then set author of resp to cn 650 | if nm is "keywords" then set tags of resp to cn 651 | end if 652 | end repeat 653 | set nname of resp to (value of XML element "title") as text -- get note title from title tag 654 | end tell 655 | end tell 656 | end tell 657 | set timerSub1 to timerSub1 + (getFineTime() - ourTimer) -- accumulate time for this subroutine 658 | return resp -- return record with meta info to caller 659 | end getMeta 660 | -- 661 | -- Outer function to read Note html file and then 662 | -- call our XML processor to parse the tag 663 | -- Input: alias of note html file 664 | -- Returns: record of values from 665 | -- 666 | on getMeta2(infile) 667 | local theXMLdata, ourTimer, resp 668 | set theXMLdata to " " & (item 1 of readIndex(infile, {""}, {""}, 2000)) & "" -- get html head tags from note html file 669 | set ourTimer to getFineTime() -- timer for start 670 | set resp to parseNote(theXMLdata) -- parse the xml data 671 | set timerSub1 to timerSub1 + (getFineTime() - ourTimer) -- accumulate time for this subroutine 672 | return resp 673 | end getMeta2 674 | -- 675 | -- Called by getMeta2 676 | -- This encapsulates the fromework "Foundation" call in 677 | -- a separate script. 678 | -- For XML processing, see https://macscripter.net/viewtopic.php?id=45479 679 | -- and https://macscripter.net/viewtopic.php?pid=200788 680 | -- and https://developer.apple.com/documentation/foundation/nsxmldocument 681 | -- and https://www.w3schools.com/xml/xpath_intro.asp 682 | -- 683 | on parseNote(xmlData) 684 | script xmlScript -- encase the asobjc call in a script object 685 | property parent : a reference to current application 686 | use framework "Foundation" 687 | on parseNote(xmlData) 688 | local theXMLDoc, theError, t, resp 689 | set resp to {nname:"", author:"", created:"", tags:"", updated:"", exporter:""} -- init returned value 690 | set {theXMLDoc, theError} to current application's NSXMLDocument's alloc()'s initWithXMLString:xmlData options:0 |error|:(reference) -- parse the xml data 691 | if theXMLDoc ≠ missing value then -- if valid xml was found 692 | -- if theXMLDoc = missing value then error (theError's localizedDescription() as text) 693 | set t to ((theXMLDoc's nodesForXPath:"/head/title" |error|:(missing value))'s valueForKey:"stringValue") as list 694 | if (length of t) > 0 then set nname of resp to (item 1 of t) -- copy Note Title if it's there 695 | set t to ((theXMLDoc's nodesForXPath:"//meta[@name='created']/@content" |error|:(missing value))'s valueForKey:"stringValue") as list 696 | if (length of t) > 0 then set created of resp to (item 1 of t) -- copy Creation Date if it's there 697 | set t to ((theXMLDoc's nodesForXPath:"//meta[@name='author']/@content" |error|:(missing value))'s valueForKey:"stringValue") as list 698 | if (length of t) > 0 then set author of resp to (item 1 of t) -- copy Author Name if it's there 699 | set t to ((theXMLDoc's nodesForXPath:"//meta[@name='updated']/@content" |error|:(missing value))'s valueForKey:"stringValue") as list 700 | if (length of t) > 0 then set updated of resp to (item 1 of t) -- copy Modification Date if it's there 701 | set t to ((theXMLDoc's nodesForXPath:"//meta[@name='keywords']/@content" |error|:(missing value))'s valueForKey:"stringValue") as list 702 | if (length of t) > 0 then set tags of resp to (item 1 of t) -- copy Tag Names if it's there 703 | set t to ((theXMLDoc's nodesForXPath:"//meta[@name='exporter-version']/@content" |error|:(missing value))'s valueForKey:"stringValue") as list 704 | if (length of t) > 0 then set exporter of resp to (item 1 of t) -- copy EN Version if it's there 705 | end if 706 | return resp 707 | end parseNote 708 | end script 709 | return xmlScript's parseNote(xmlData) 710 | end parseNote 711 | -- 712 | -- Make text string HTML safe (for Note titles, Notebook Names, Tag names, etc.) 713 | -- 714 | on textHT(inString) 715 | local j 716 | set j to repl(inString, "&", "&") -- must be first to avoid double-encoding of ampersands 717 | set j to repl(j, "\"", """) 718 | set j to repl(j, "'", "'") 719 | set j to repl(j, "<", "<") 720 | set j to repl(j, ">", ">") 721 | return j 722 | end textHT 723 | -- 724 | -- Replace one string with another 725 | -- 726 | on repl(theText, theSearchString, theReplacementString) 727 | local theTextItems 728 | set AppleScript's text item delimiters to theSearchString 729 | set theTextItems to every text item of theText 730 | set AppleScript's text item delimiters to theReplacementString 731 | set theText to theTextItems as string 732 | set AppleScript's text item delimiters to "" 733 | return theText 734 | end repl 735 | -- 736 | -- Return formatted date and time as yyyy-mm-dd hh-mm 737 | -- input is a date object 738 | -- 739 | on fmtLongDate(inpDate) 740 | set {year:y, month:m, day:d, hours:h, minutes:r} to inpDate 741 | return (y as text) & "-" & lzer(m as number) & "-" & lzer(d as text) & " " & lzer(h as text) & ":" & lzer(r as text) 742 | end fmtLongDate 743 | -- 744 | -- Return an integer as two digits, with leading zero if necessary and negative sign if < 0 745 | -- 746 | on lzer(numb) 747 | if numb ≥ 0 then 748 | return (text -1 thru -2 of ("000" & (numb as text))) 749 | else 750 | return "-" & (text -1 thru -2 of ("000" & ((-numb) as text))) 751 | end if 752 | end lzer 753 | -- 754 | -- Return time zone offset as -hh:mm 755 | -- 756 | on tzone() 757 | return lzer(offsetGMT div hours) & ":" & lzer(offsetGMT mod hours / minutes as integer) 758 | end tzone 759 | -- 760 | -- Return a date object 761 | -- input is a date/time string from an HTML meta tag 762 | -- as 2020-11-22 21:05:34 +0000 763 | -- 764 | on getDateMeta(x) 765 | local d 766 | set d to initDate(2000, 1, 1) -- 0.4.2 767 | if x = "" then return d 768 | tell d to set {its year, its month, its day, its time} to {0 + (text 1 thru 4 of x), 0 + (text 6 thru 7 of x), 0 + (text 9 thru 10 of x), (hours * (0 + (text 12 thru 13 of x))) + (minutes * (0 + (text 15 thru 16 of x))) + (0 + (text 18 thru 19 of x))} -- 769 | set d to d + (hours * (text 22 thru 23 of x)) + (0 + (text 24 thru 25 of x)) * (cond((text 21 thru 21 of x) = "-", -1, 1)) + (time to GMT) 770 | return d 771 | end getDateMeta 772 | -- 0.4.2 .... 773 | -- Initialize a date object 774 | -- input is year, month, day as integers 775 | -- returns date object 776 | -- Avoids locale-related issues with dates in strings 777 | -- 778 | on initDate(yr, mo, dy) -- 0.4.2 779 | local md 780 | set md to current date 781 | tell md to set {its year, its month, its day, its time} to {yr, mo, dy, hours * 12} 782 | return md 783 | end initDate 784 | -- .... 0.4.2 785 | -- 786 | -- Return a date object in local time 787 | -- input is a date/time string as in our bookexported comment 788 | -- and in Evernote's search syntax: 20210205T203848Z 789 | -- With help from https://forum.latenightsw.com/t/time-to-gmt-for-any-date/2070/2 790 | -- to calc GMT offset for arbitrary day of the year 791 | -- and: http://www.unicode.org/reports/tr35/tr35-31/tr35-dates.html#Date_Format_Patterns 792 | -- for info on date pattern strings 793 | -- 794 | on getDateSrch(x) 795 | script dateScript 796 | property parent : a reference to current application 797 | use framework "Foundation" 798 | on getDateSrch(x) 799 | local df, theDate, tz, theOffset, d 800 | if x = "" then return initDate(2000, 1, 1) -- 0.4.2 801 | set df to current application's NSDateFormatter's new() 802 | df's setDateFormat:"yyyyMMdd'T'HHmmssX" -- Pattern date 803 | set theDate to df's dateFromString:x 804 | -- get offset 805 | set tz to current application's NSTimeZone's localTimeZone() 806 | set theOffset to tz's secondsFromGMTForDate:theDate 807 | -- I should be able to add theDate to theOffset, but Applescript could not seem to 808 | -- constrain theDate from NSDate to Date. So, I'm converting it another way.... 809 | set d to current date -- create a date variable 0.4.2 810 | tell d to set {its year, its month, its day, its time} to {0 + (text 1 thru 4 of x), 0 + (text 5 thru 6 of x), 0 + (text 7 thru 8 of x), (hours * (0 + (text 10 thru 11 of x))) + (minutes * (0 + (text 12 thru 13 of x))) + (0 + (text 14 thru 15 of x))} -- convert date string to date object 811 | set d to d + theOffset -- adjust GMT time string to local time 812 | return d -- return date in our time zone 813 | end getDateSrch 814 | end script 815 | return dateScript's getDateSrch(x) 816 | end getDateSrch 817 | -- 818 | -- Write an index.html file 819 | -- 820 | on writeIndex(thePath, theText) 821 | set theAlias to thePath as POSIX file as «class furl» 822 | try 823 | -- tell application "Finder" (adding class furl below fixed this) 824 | -- using terms from scripting additions. (adding class furl below fixed this) 825 | set theOpenedFile to open for access theAlias as «class furl» with write permission 826 | -- end tell 827 | -- end using terms from 828 | -- if logProgress then log ("response from open: " & theOpenedFile) 829 | set eof of theOpenedFile to 0 830 | write theText to theOpenedFile as «class utf8» 831 | -- if logProgress then log "Normal file close" 832 | close access theOpenedFile 833 | return true 834 | on error errMsg number errNum 835 | -- if logProgress then log "Error file close. Msg: " & errMsg & " Num: " & errNum 836 | try 837 | close access file theAlias 838 | end try 839 | return false 840 | end try 841 | -- if logProgress then log "Leaving addtolog" 842 | end writeIndex 843 | -- 844 | -- Format file/folder size for display within a table cell 845 | -- Input is a number of bytes 846 | -- Returns a table cell () formatted with file size 847 | -- 848 | on fmtSize(s) 849 | return " " & makeSize(s) & " 850 | " 851 | end fmtSize 852 | -- 853 | -- Format file/folder size for display 854 | -- Input is a number of bytes 855 | -- Returns a string with file size 856 | -- We're using decimal calc for KB, MB and GB, in keeping 857 | -- with how macOS Finder reports sizes 858 | -- This newer version of the handler adds one decimal point to the result 859 | -- 860 | on makeSize(s) 861 | local strSize 862 | if s < 1000 then -- Build a formatted text showing total size 863 | set strSize to "0." & ((s / 100) as integer as text) & " KB" 864 | else if (s / 1000) < 1000 then 865 | set strSize to ((s / 100) as integer as text) 866 | set strSize to (text 1 thru -2 of strSize) & "." & (text -1 thru -1 of strSize) & " KB" 867 | else if (s / (1000 ^ 2)) < 1000 then 868 | set strSize to ((s / (100000)) as integer as text) 869 | set strSize to (text 1 thru -2 of strSize) & "." & (text -1 thru -1 of strSize) & " MB" 870 | else 871 | set strSize to ((s / (1000 ^ 3 / 10)) as integer as text) 872 | set strSize to (text 1 thru -2 of strSize) & "." & (text -1 thru -1 of strSize) & " GB" 873 | end if 874 | return strSize 875 | end makeSize 876 | -- 877 | -- Format file/folder size for display 878 | -- Input is a number of bytes 879 | -- Returns a string with file size 880 | -- We're using decimal calc for KB, MB and GB, in keeping 881 | -- with how macOS Finder reports sizes 882 | -- 883 | on makeSizeOld(s) 884 | local strSize 885 | if s < 1000 then -- Build a formatted text showing total size 886 | set strSize to (s as integer as text) & " B  " 887 | else if (s / 1000) < 1000 then 888 | set strSize to ((s / 1000) as integer as text) & " KB" 889 | else if (s / (1000 ^ 2)) < 1000 then 890 | set strSize to ((s / (1000 ^ 2)) as integer as text) & " MB" 891 | else 892 | set strSize to ((s / (1000 ^ 3)) as integer as text) & " GB" 893 | end if 894 | return strSize 895 | end makeSizeOld 896 | 897 | -- Handler for Property List file. Three functions are performed in this one routine 898 | -- so that the relevant variables can be localized here. 899 | -- Option 1: Create Plist if necessary then fetch current property values 900 | -- into global variables. 901 | -- if PList already exists, return True; if we had to create it, return false 902 | -- Option 2: Prompt user to update properties one after the other. 903 | -- returns FALSE if user cancels or fails to correctly set the properties 904 | -- Option 3: Update the PList with a new ExportDate 905 | -- returns True 906 | on managePlist(opt) 907 | local plFolderHTML, plFolderENEX, plAllNotebooks, plSearchString 908 | local plExportAsHTML, plExportAsENEX, plBooksSelected, plPropDate 909 | local plExportDate, plTimeoutMinutes, foundPList, theParentDictionary 910 | local thePropertyListFile, myResp, exopt, buts, chc, bookNames 911 | local msgPart, resp 912 | -- Names of Property List items 913 | set plFolderHTML to "FolderHTML" -- Folder for HTML exports 914 | set plFolderENEX to "FolderENEX" -- Folder for ENEX exports 915 | set plAllNotebooks to "AllNotebooks" -- Export all notebooks, not just some 916 | set plSearchString to "SearchString" -- user's filter for notes 917 | set plExportAsHTML to "ExportAsHTML" -- Export as HTML? 918 | set plExportAsENEX to "ExportAsENEX" -- Export as ENEX? 919 | set plBooksSelected to "BooksSelected" -- names of notebooks to export 920 | set plPropDate to "PropDate" -- date this Property List last updated 921 | set plExportDate to "ExportDate" -- date export last completed 922 | set plTimeoutMinutes to "TimeoutMinutes" -- export timeout in minutes 923 | 924 | if opt = 1 then -- Option 1: create default PList if necessary and then load Plist values into Global variables 925 | -- set foundPList to true if PList exisits 926 | tell application "Finder" to set foundPList to (exists myPListFullPath as POSIX file) -- Does PList exist? 927 | if not foundPList then -- If it doesn't exist, create one with default values 928 | tell application "System Events" 929 | set theParentDictionary to make new property list item with properties {kind:record} 930 | set thePropertyListFile to make new property list file with properties {contents:theParentDictionary, name:myPListFullPath} 931 | tell property list items of thePropertyListFile 932 | make new property list item at end with properties {kind:string, name:plFolderHTML, value:FolderHTML} 933 | make new property list item at end with properties {kind:string, name:plFolderENEX, value:FolderENEX} 934 | make new property list item at end with properties {kind:boolean, name:plAllNotebooks, value:AllNotebooks} 935 | make new property list item at end with properties {kind:string, name:plSearchString, value:SearchString} 936 | make new property list item at end with properties {kind:boolean, name:plExportAsHTML, value:ExportAsHTML} 937 | make new property list item at end with properties {kind:boolean, name:plExportAsENEX, value:ExportAsENEX} 938 | make new property list item at end with properties {kind:list, name:plBooksSelected, value:BooksSelected} 939 | make new property list item at end with properties {kind:date, name:plPropDate, value:(current date)} 940 | make new property list item at end with properties {kind:date, name:plExportDate, value:ExportDate} 941 | make new property list item at end with properties {kind:number, name:plTimeoutMinutes, value:TimeoutMinutes} 942 | end tell 943 | end tell 944 | end if 945 | -- Fetch current PList values from the file 946 | tell application "System Events" 947 | tell property list file myPListFullPath 948 | set FolderHTML to value of property list item plFolderHTML 949 | set FolderENEX to value of property list item plFolderENEX 950 | set AllNotebooks to value of property list item plAllNotebooks 951 | set SearchString to value of property list item plSearchString 952 | set ExportAsHTML to value of property list item plExportAsHTML 953 | set ExportAsENEX to value of property list item plExportAsENEX 954 | set BooksSelected to value of property list item plBooksSelected as list 955 | set ExportDate to value of property list item plExportDate 956 | set TimeoutMinutes to value of property list item plTimeoutMinutes 957 | end tell 958 | end tell 959 | return foundPList -- Tell caller if PList alreay existed 960 | end if 961 | -- 962 | -- This option prompts the user to review / update the properties 963 | -- 964 | if opt = 2 then -- Option 2: Prompt for updates to Property List items 965 | set myResp to false -- assume update has failed 966 | tell application "System Events" to set frontProcess to the name of every process whose frontmost is true 967 | set exopt to 3 -- assume both formats 968 | if (not (ExportAsHTML and ExportAsENEX)) then set exopt to cond(not ExportAsHTML, 2, 1) -- if not HTML, then ENEX only 969 | set exopt to item_position(choose from list plExportTypeDesc with title "Choose export types" with prompt "Which format(s) should be exported?" default items {item exopt of plExportTypeDesc}, plExportTypeDesc) 970 | if logProgress then log "Export option: " & exopt 971 | if exopt = 0 then return myResp -- quit if user cancelled 972 | set ExportAsHTML to item exopt of {true, false, true} -- Set HTML export flag 973 | set ExportAsENEX to item exopt of {false, true, true} -- Set ENEX export flag 974 | if logProgress then log "ExportAsHTML: " & ExportAsHTML & " ExportAsENEX: " & ExportAsENEX 975 | if ExportAsHTML then -- If exporting as HTML 976 | -- first, check that volume containing default output folder is mounted 977 | -- use Documents if user says to ignore prior setting 978 | if checkPropVol(FolderHTML, "HTML") then set FolderHTML to path to documents folder as text 979 | tell application "Finder" to set FolderHTML to (choose folder with prompt "Select or create folder for HTML exports" default location alias FolderHTML) as text -- prompt for folder 980 | end if 981 | if logProgress then log "FolderHTML: " & FolderHTML 982 | if ExportAsENEX then -- if exporting as ENEX 983 | -- first, check that volume containing default output folder is mounted 984 | -- use Documents if user says to ignore prior setting 985 | if checkPropVol(FolderENEX, "ENEX") then set FolderENEX to path to documents folder as text 986 | tell application "Finder" to set FolderENEX to (choose folder with prompt "Select or create folder for ENEX exports" default location alias FolderENEX) as text -- prompt for folder 987 | end if 988 | if logProgress then log "FolderENEX: " & FolderENEX 989 | activate frontProcess -- switch back from Finder to script 990 | set buts to {"All notebooks", "Only selected notebooks", "Cancel"} 991 | set chc to display dialog "Export all notebooks or only selected ones?" buttons buts default button cond(AllNotebooks, 1, 2) cancel button 3 with icon note 992 | set AllNotebooks to ((item 1 of buts) = (button returned of chc)) 993 | if logProgress then log "AllNotebooks: " & AllNotebooks 994 | if not AllNotebooks then -- if only selected notebooks 995 | -- set bookNames to sortList(getBooks("")) -- sort notebook names 0.3.2 996 | set bookNames to getBooks() -- get sorted notebook names 997 | if logProgress then log "Count of Notebooks in Evernote: " & (count of bookNames) 998 | set BooksSelected to choose from list bookNames with title "Choose Notebooks to export" with prompt "Use command key (⌘) to select several or all" default items BooksSelected with multiple selections allowed 999 | if BooksSelected = false then return myResp -- if user cancelled, return to caller w 'false' 1000 | if logProgress then log "BooksSelected: " & BooksSelected 1001 | end if 1002 | set resp to display dialog "Enter Evernote search terms to restrict notes that will be exported. Do not use 'notebook:', 'any:', or 'updated:' terms. Leave blank to export all notes in each notebook." default answer SearchString with title "Optional search terms" with icon note 1003 | set SearchString to (text returned of resp) as text -- save user input 1004 | set msgPart to "" -- clear error message fragment 1005 | repeat -- loop to prompt for Timeout value and validate it 1006 | set resp to display dialog ("Enter export timeout in minutes. " & msgPart) default answer (TimeoutMinutes as text) with title "Specify Timeout" with icon note 1007 | set msgPart to "" -- clear error message fragment 1008 | try -- see if text entered is a valid integer 1009 | if logProgress then log "About to convert to integer." 1010 | set TimeoutMinutes to (text returned of resp) as integer 1011 | if logProgress then log "Convert to integer worked: " & TimeoutMinutes 1012 | on error -- not a valid integer 1013 | set msgPart to " 1014 | Timeout value must be a positive integer." -- set error message 1015 | if logProgress then log "Convert to integer failed." 1016 | set TimeoutMinutes to (text returned of resp) -- leave bad timeout value in place 1017 | end try 1018 | if logProgress then log "TimeoutMinutes: " & TimeoutMinutes 1019 | if msgPart = "" then 1020 | if TimeoutMinutes < 1 then 1021 | set msgPart to " 1022 | Timeout value must be a positive integer." -- set error message 1023 | else 1024 | exit repeat -- if integer was valid, break out of the Repeat loop 1025 | end if 1026 | end if 1027 | end repeat 1028 | -- 1029 | -- With all Properties now reviewed by user, write them 1030 | -- to the Property List file 1031 | -- 1032 | tell application "System Events" 1033 | tell property list file myPListFullPath 1034 | set value of property list item plFolderHTML to FolderHTML 1035 | set value of property list item plFolderENEX to FolderENEX 1036 | set value of property list item plAllNotebooks to AllNotebooks 1037 | set value of property list item plSearchString to SearchString 1038 | set value of property list item plExportAsHTML to ExportAsHTML 1039 | set value of property list item plExportAsENEX to ExportAsENEX 1040 | set value of property list item plTimeoutMinutes to TimeoutMinutes 1041 | set value of property list item plBooksSelected to BooksSelected 1042 | set value of property list item plPropDate to current date 1043 | end tell 1044 | end tell 1045 | if logProgress then log "Successfully set all properties" 1046 | return true 1047 | end if 1048 | -- 1049 | -- This option saves the date of the last successful export 1050 | -- 1051 | if opt = 3 then -- Option 3: Update only the ExportDate property 1052 | tell application "System Events" 1053 | tell property list file myPListFullPath 1054 | set value of property list item plExportDate to ExportDate 1055 | end tell 1056 | end tell 1057 | return true 1058 | end if 1059 | end managePlist 1060 | -- 1061 | -- Get list of all notebooks from Evernote 1062 | -- Return a sorted list 1063 | -- 1064 | on getBooks() 1065 | local listBooks 1066 | set listBooks to {} -- init the list of book names 1067 | tell application "Evernote" 1068 | repeat with iBook in every notebook -- for every notebook ... 1069 | copy (name of iBook) to end of listBooks -- add notebook name to list 1070 | end repeat 1071 | end tell 1072 | return sortListOfStrings(listBooks) -- sort the list and return it 1073 | end getBooks 1074 | -- 1075 | -- Sort a list of strings 1076 | -- based on https://macosxautomation.com/applescript/sbrt/sbrt-00.html 1077 | -- converted to a Script object to avoid using framework "Foundation" in the main script 1078 | -- 1079 | on sortListOfStrings(sourceList) 1080 | script sortScript -- encase the asobjc call in a script object 1081 | property parent : a reference to current application 1082 | use framework "Foundation" 1083 | on sortListOfStrings(sourceList) 1084 | -- create a Cocoa array from the passed AppleScript list 1085 | set the CocoaArray to current application's NSArray's arrayWithArray:sourceList 1086 | -- sort the Cocoa array 1087 | set the sortedItems to CocoaArray's sortedArrayUsingSelector:"localizedStandardCompare:" 1088 | -- return the Cocoa array coerced to an AppleScript list 1089 | return (sortedItems as list) 1090 | end sortListOfStrings 1091 | end script 1092 | return sortScript's sortListOfStrings(sourceList) 1093 | end sortListOfStrings 1094 | -- 1095 | -- Return time since Jan 1, 2001 in millionths of a second 1096 | -- 1097 | on getFineTime() 1098 | script timeScript -- encase the asobjc call in a script object 1099 | property parent : a reference to current application 1100 | use framework "Foundation" 1101 | on getFineTime() 1102 | return current application's CFAbsoluteTimeGetCurrent() 1103 | end getFineTime 1104 | end script 1105 | return timeScript's getFineTime() 1106 | end getFineTime 1107 | -- 1108 | -- Return the item number of an item in a list 1109 | -- 1110 | on item_position(myitem, theList) 1111 | if myitem = false then return 0 1112 | repeat with i from 1 to the count of theList 1113 | if (item i of theList) is (item 1 of myitem) then return i 1114 | end repeat 1115 | return 0 1116 | end item_position 1117 | -- 1118 | -- Format a note Tag by enclosing it in html span tags 1119 | -- 1120 | on fmtTag(tn) 1121 | -- wrap tagname in a Span, replacing blanks with non-breaking space to 1122 | -- hopefully prevent them from breaking across lines in notebook index page 1123 | return "" & repl(textHT(tn), " ", " ") & " " 1124 | end fmtTag 1125 | -- 1126 | -- Format a table cell containg a date 1127 | -- The local date will be visible in the cell and 1128 | -- the GMT date & time will be in a data attribute for use by 1129 | -- javascript, which will update the cell value and create a title (tool tip) 1130 | -- 1131 | on dateCell(dt) 1132 | local y, m, d, yy, mm, dd, hh, s, r 1133 | set {year:y, month:m, day:d} to dt -- for short, local date that will visible in cell 1134 | -- return (y as text) & "-" & lzer(m as number) & "-" & lzer(d as text) 1135 | set {year:yy, month:mm, day:dd, hours:hh, minutes:r, seconds:s} to (dt - offsetGMT) -- for javascript to convert 1136 | -- return (yy as text) & "-" & lzer(mm as number) & "-" & lzer(dd as text) & "T" & lzer(hh as text) & ":" & lzer(r as text) & ":" & lzer(s as text) & "Z" 1137 | return " " & ((y as text) & "-" & lzer(m as number) & "-" & lzer(d as text)) & " 1138 | " 1139 | end dateCell 1140 | -- 1141 | -- Format a page header with left and right content 1142 | -- 1143 | on pageHead(cleft, cright, cinstruct, checkbox) 1144 | return "
1145 |
" & cleft & "
1146 |
" & cright & "
1147 |
 
1148 |
" & cinstruct & "
1149 | " & cond(checkbox, "
1150 |
1151 | 1152 |
", "") & " 1153 |
 
1154 |
1155 | " 1156 | end pageHead 1157 | -- 1158 | -- Return a value conditionally 1159 | -- 1160 | on cond(tst, v1, v2) 1161 | if tst then return v1 1162 | return v2 1163 | end cond 1164 | -- 1165 | -- Get a text strings from an existing file 1166 | -- fname = full name of input file 1167 | -- frontstr = list of leading tokens to scan for 1168 | -- backstr = list of trailing tokens to scan for 1169 | -- chrs = bytes of file to read, or 0 for entire file 1170 | -- Returns list of strings found with "" for strings not found 1171 | -- 1172 | on readIndex(fname, frontstr, backstr, chrs) 1173 | local ex, xd, chrs, res, i, stpos, bal, enpos, infile 1174 | try 1175 | if logProgress then log "Reading file: " & fname & " for " & chrs & " bytes" 1176 | end try 1177 | tell application "Finder" to set ex to (exists fname) -- check for file existence 1178 | if not ex then -- if file does not exist 1179 | set xd to "dummy string" -- set file data to a dummy string 1180 | else -- otherwise, read the input file 1181 | try 1182 | set fname to fname as alias 1183 | end try 1184 | if chrs = 0 then 1185 | --set xd to read (fname as «class furl») as «class utf8» -- read entire file 1186 | set xd to read fname as «class utf8» -- read entire file 1187 | else 1188 | --set xd to read (fname as «class furl») for chrs as «class utf8» -- if caller specified a byte count 1189 | repeat with ei from chrs to (chrs + 7) -- try several reads in case UTF8 char is split 1190 | set readOK to true 1191 | try 1192 | set xd to read fname for ei as «class utf8» -- if caller specified a byte count 1193 | on error 1194 | set readOK to false 1195 | end try 1196 | if readOK then exit repeat 1197 | set xd to "dummy string" -- set file data to a dummy string 1198 | end repeat 1199 | end if 1200 | end if 1201 | if logProgress then log " Len read: " & length of xd 1202 | set res to {} -- initialize list to be returned 1203 | repeat with i from 1 to (length of frontstr) -- for each leading search token 1204 | -- if logProgress then log " frontstr: " & i & " " & item i of frontstr 1205 | -- if logProgress then log " backstr : " & i & " " & item i of backstr 1206 | set stpos to offset of (item i of frontstr) in xd -- find front token 1207 | if stpos = 0 then 1208 | copy "" to end of res -- return empty string if not found 1209 | else 1210 | set stpos to stpos + (length of (item i of frontstr)) -- start of target text 1211 | set bal to text stpos thru (length of xd) of xd -- remainder of file data 1212 | set enpos to (offset of (item i of backstr) in bal) - 1 1213 | if enpos = 0 then 1214 | copy "" to end of res -- return empty string if not found 1215 | else 1216 | copy trim(text 1 thru enpos of bal) to end of res 1217 | end if 1218 | end if 1219 | -- if logProgress then log " result(" & (item i of res) & ")" 1220 | end repeat 1221 | return res 1222 | end readIndex 1223 | -- 1224 | -- Trim leading and trailing blanks from a string 1225 | -- 1226 | -- From Nigel Garvey's CSV-to-list converter 1227 | -- http://macscripter.net/viewtopic.php?pid=125444#p125444 1228 | on trim(txt) 1229 | repeat with i from 1 to (count txt) - 1 1230 | if (txt begins with space) then 1231 | set txt to text 2 thru -1 of txt 1232 | else 1233 | exit repeat 1234 | end if 1235 | end repeat 1236 | repeat with i from 1 to (count txt) - 1 1237 | if (txt ends with space) then 1238 | set txt to text 1 thru -2 of txt 1239 | else 1240 | exit repeat 1241 | end if 1242 | end repeat 1243 | if (txt is space) then set txt to "" 1244 | return txt 1245 | end trim 1246 | -- 1247 | -- Check that the volume is mounted before running an export 1248 | -- Then look for folder. 1249 | -- If both found, return false 1250 | -- 1251 | on checkRunVol(fpath, omode) 1252 | local diskNames, targetVolume, ex 1253 | set targetVolume to text 1 thru ((offset of ":" in fpath) - 1) of fpath 1254 | repeat 1255 | tell application "System Events" to set diskNames to name of every disk 1256 | if logProgress then log diskNames 1257 | if (diskNames contains targetVolume) then exit repeat 1258 | set ret to display alert "Volume not mounted" message "The volume '" & targetVolume & "' needed for " & omode & " export is not mounted. Please mount it and then retry. Or click cancel." buttons {"Retry", "Cancel"} default button 1 cancel button 2 1259 | end repeat 1260 | tell application "Finder" to set ex to (exists fpath) 1261 | if not ex then display alert "Folder " & (POSIX path of fpath) & " not found for " & omode & " export. 1262 | Rerun this script and choose Change Settings to set " & omode & " export folder." 1263 | return ex 1264 | end checkRunVol 1265 | -- 1266 | -- Check that the volume is mounted before prompting user to select the output folder 1267 | -- Return False if found or true if not found. 1268 | -- 1269 | on checkPropVol(fpath, omode) 1270 | set targetVolume to text 1 thru ((offset of ":" in fpath) - 1) of fpath 1271 | repeat 1272 | tell application "System Events" to set diskNames to name of every disk 1273 | if logProgress then log diskNames 1274 | if (diskNames contains targetVolume) then exit repeat 1275 | set ret to display alert "Volume not mounted" message "The volume '" & targetVolume & "' selected for " & omode & " export is not mounted. Please mount it and then retry or choose another folder or click cancel." buttons {"Retry", "Choose another", "Cancel"} default button 1 cancel button 3 1276 | if button returned of ret = "Choose another" then return true 1277 | end repeat 1278 | tell application "Finder" to set ex to (exists fpath) 1279 | activate frontProcess -- bring us to the front 1280 | if not ex then display alert "Folder previously selected for " & omode & " export not found (" & (POSIX path of fpath) & "). 1281 | Choose another folder." 1282 | return not ex 1283 | end checkPropVol 1284 | -- 1285 | -- Return a comment string with system info 1286 | -- to embed as a comment in the index.html pages 1287 | -- A comment string from prior index file may be supplied 1288 | -- 1289 | on getSysInfo(pastEnv) 1290 | local sys, ENacct, ENver 1291 | if length of pastEnv > 0 then return " 1292 | " -- reuse prior comment 1293 | if headerInfo is "" then 1294 | set sys to system info 1295 | tell application "Evernote" 1296 | set ENacct to name of current account 1297 | set ENver to version 1298 | end tell 1299 | set headerInfo to " 1303 | " 1304 | end if 1305 | return headerInfo 1306 | end getSysInfo 1307 | -- 1308 | -- Encode a string for use as a URL in HTML (0.3.2) 1309 | -- replaces blanks and most special characters with hex codes 1310 | -- This is encased in a Script object as described here: 1311 | -- https://latenightsw.com/adding-applescriptobjc-to-existing-scripts/ 1312 | -- so that we can avoid use framework "Foundation" which messes 1313 | -- up the Read and Write commands 1314 | -- 1315 | on urlEncode(inp) 1316 | script aScript 1317 | property parent : a reference to current application 1318 | use framework "Foundation" 1319 | on urlEncode(inp) 1320 | tell current application's NSString to set rawUrl to stringWithString_(inp) 1321 | set theEncodedURL to rawUrl's stringByAddingPercentEscapesUsingEncoding:4 -- 4 is NSUTF8StringEncoding 1322 | return theEncodedURL as Unicode text 1323 | end urlEncode 1324 | end script 1325 | return aScript's urlEncode(inp) 1326 | end urlEncode 1327 | -- 1328 | -- WriteJS handler - if the enscripts.js file doesn't exist, write it to 1329 | -- the top level HTML folder 1330 | -- 1331 | on writeJS() 1332 | set jsFile to (POSIX path of FolderHTML) & myJSfile -- name of javascript file 1333 | tell application "Finder" to set foundJS to (exists jsFile as POSIX file) -- Does PList exist? 1334 | if foundJS then return false 1335 | set js to "/* 1336 | This JS file is created initially by the applescript 1337 | Notebook Export Manager For Evernote and placed in 1338 | the top-level html folder of exported Notebooks. 1339 | -- Once created, the user can modify this file, and 1340 | the applescript will not over-write it. 1341 | -- This JS file contains the functions to dynamically 1342 | sort the main html table and perform other 1343 | run-time functions inside the top-level 1344 | Notebook list webpage and the lower-level 1345 | Note list index pages. 1346 | -- If you want to modify this JS script and re-imbed 1347 | it into the applescript, avoid using any backslash 1348 | or double-quote characters, as they must be escaped 1349 | in applescript text constants. 1350 | */ 1351 | function setLocalDates() { 1352 | /* This function is to be called by the OnLoad tag 1353 | of a BODY element. It loops through every TD 1354 | (Table Data) element, looking for the data-gmtdate 1355 | attribute, taking the GMT date stored there and 1356 | using it to populate the innerHTML and the title 1357 | of the table cell. So, dates will be displayed 1358 | in the user's local time zone. 1359 | */ 1360 | var elems,c,d,i,s 1361 | elems = document.getElementsByTagName('TD') 1362 | for (i = 0; i < (elems.length); i++) { 1363 | c = elems[i].getAttribute('data-gmtdate'); 1364 | if (c) { 1365 | d = new Date(c); 1366 | s = d.getFullYear() + '-' + lz(d.getMonth() + 1) + '-' + lz(d.getDate()) + ' ' + lz(d.getHours()) + ':' + lz(d.getMinutes()) ; 1367 | elems[i].innerText = s.substring(0, 10); 1368 | elems[i].title = s; 1369 | } 1370 | } 1371 | } 1372 | function lz(n) { 1373 | /* format an integer as two digits with leading zero */ 1374 | var s = '00' + n; 1375 | return s.substring(s.length - 2); 1376 | } 1377 | function sortTable(tname,col,typ) { 1378 | /* 1379 | Sort table using javascript array sort 1380 | based on https://stackoverflow.com/questions/14267781/sorting-html-table-with-javascript 1381 | tname is the ID of the table 1382 | col is the column number to sort on (0 to n) 1383 | typ is the type of data (see Switch statement below 1384 | */ 1385 | var sortattr = 'data-sort', i ; // data attribute tag name 1386 | var tabl = document.getElementById(tname) ; // get table object 1387 | var tb = tabl.tBodies[0] ; // get table tbody object 1388 | var thdr = tabl.tHead.rows[0].cells ; // get cells in table header row 1389 | var sd = thdr[col].getAttribute(sortattr) == 'A' ? -1 : 1 ; // get sort direction (A/D) (A if no prior sort) 1390 | var tr = Array.prototype.slice.call(tb.rows, 0) ; // get rows in array 1391 | for (i = 0; i < (thdr.length); i++) { 1392 | thdr[i].classList.remove('trasc','trdesc'); /* clear table header classes */ 1393 | thdr[i].setAttribute(sortattr, 'U'); // set all cols sort direction to unsorted 1394 | } 1395 | // Sort the array of table rows based on specified cell (ie, column) and data type 1396 | tr = tr.sort(function(a,b) { 1397 | var cm, la, lb 1398 | la = a.cells[col] ; // get cell being sorted 1399 | lb = b.cells[col] ; // get cell being sorted 1400 | switch(typ) { 1401 | case 1: // sort column contains integers 1402 | cm = (parseInt(la.innerHTML) > parseInt(lb.innerHTML)) ? 1 : -1 ; 1403 | break ; 1404 | case 2: // sort column contains number in an attribute 1405 | cm = (parseFloat(la.getAttribute('data-foldersize')) > parseFloat(lb.getAttribute('data-foldersize'))) ? 1 : -1 ; 1406 | break ; 1407 | case 3: // sort column contains GMT date in an attribute 1408 | cm = (la.getAttribute('data-gmtdate') > lb.getAttribute('data-gmtdate')) ? 1 : -1 ; 1409 | break ; 1410 | default: // sort column contains a string, possibly inside an tag 1411 | la = ((null !== la.firstElementChild) ? la.firstElementChild : la).innerHTML.toLowerCase() ; 1412 | lb = ((null !== lb.firstElementChild) ? lb.firstElementChild : lb).innerHTML.toLowerCase() ; 1413 | cm = la > lb ? 1 : -1 ; 1414 | } 1415 | return cm * sd ; // return comparison result, adjusted for sort direction 1416 | }) ; 1417 | for(i = 0; i < tr.length; ++i) tb.appendChild(tr[i]); // append each row in order back into tbody 1418 | thdr[col].classList.add(sd == -1 ? 'trdesc' : 'trasc') ; // set color indicator in sort column 1419 | thdr[col].setAttribute(sortattr, (sd == -1 ? 'D' : 'A')); // remember sorted column 1420 | } 1421 | function hideNBnames(cls){ 1422 | /* This function is used to inject CSS into all the index.html 1423 | pages for individual notebooks, to hide table columns 1424 | with specified class. 1425 | */ 1426 | var style2 = document.createElement('style'); 1427 | style2.innerHTML = '.' + cls + ' { ' + 1428 | ' display:none; ' + 1429 | ' } ' ; 1430 | document.head.appendChild(style2); 1431 | } 1432 | function settarget(checkid,acls) { 1433 | /* This function sets the target= attribute of anchor 1434 | tags with the class 'acls' based on the checked 1435 | property of the element with id 'checkid' 1436 | */ 1437 | var atarg, elems, i 1438 | atarg = document.getElementById(checkid).checked ? '_blank' : '_self' ; 1439 | elems = document.getElementsByClassName(acls) ; 1440 | for (i = 0; i < (elems.length); i++) { 1441 | elems[i].target = atarg ; 1442 | } 1443 | }" 1444 | writeIndex(jsFile, js) 1445 | return true 1446 | end writeJS 1447 | -- 1448 | -- WriteCSS handler - if the enstyles.css file doesn't exist, write it to 1449 | -- the top level HTML folder 1450 | -- 1451 | on writeCSS() 1452 | set cssFile to (POSIX path of FolderHTML) & myCSSfile -- name of css file 1453 | tell application "Finder" to set foundCSS to (exists cssFile as POSIX file) -- Does CSS exist? 1454 | if foundCSS then return false 1455 | set css to "/* 1456 | This CSS stylesheet is created initially by the applescript 1457 | Notebook Export Manager to control the display 1458 | of index.html pages for exported notebooks and 1459 | for the top-level index page. 1460 | */ 1461 | body { 1462 | font-family: Arial, Helvetica, sans-serif; 1463 | } 1464 | .clnote { 1465 | text-align: center; 1466 | } 1467 | .clfile { 1468 | text-align: center; 1469 | } 1470 | .clsize { 1471 | text-align: right; 1472 | } 1473 | .tdate { 1474 | text-align: center; 1475 | } 1476 | table { 1477 | border-collapse: collapse; 1478 | border-spacing: 0; 1479 | font-size: 14px; 1480 | } 1481 | th { 1482 | cursor: pointer ; 1483 | } 1484 | tbody tr:nth-child(odd) { 1485 | background: #eee; 1486 | } 1487 | th, td { 1488 | padding:4px; 1489 | } 1490 | td a { 1491 | display:block; 1492 | width:100%; 1493 | } 1494 | .trasc { 1495 | background-color: #ccff99; 1496 | } 1497 | .trdesc { 1498 | background-color: #ff9999; 1499 | } 1500 | .ENtag { 1501 | background-color: #B7D7FF; 1502 | } 1503 | .instruct { 1504 | padding-bottom: 15px; 1505 | font-size: 14px; 1506 | color: gray; 1507 | float: left; 1508 | } 1509 | .inpform { 1510 | font-size: 14px; 1511 | background-color: lightgray ; 1512 | float: right ; 1513 | padding: 3px 5px 3px 5px ; 1514 | } 1515 | .lhdr { 1516 | float: left; 1517 | font-size: 24px; 1518 | } 1519 | .rhdr { 1520 | float: right; 1521 | font-size: 18px; 1522 | } 1523 | .bhdr { 1524 | clear:both; 1525 | bottom-margin: 15px ; 1526 | } 1527 | .hdr2 { 1528 | font-size: 18px; 1529 | padding-top: 20px; 1530 | padding-bottom: 20px; 1531 | } 1532 | .notecol { 1533 | width:40%; 1534 | } 1535 | .linkhover:hover { 1536 | background-color: #99ccff; 1537 | } 1538 | .zeroNotes { 1539 | background-color: #ffff99; 1540 | } 1541 | " 1542 | writeIndex(cssFile, css) 1543 | return true 1544 | end writeCSS 1545 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Notebook Export Manager 2 | 3 | This describes an AppleScript program that helps create and maintain HTML and ENEX exports of Evernote® notebooks in macOS. Evernote is a trademark of Evernote Corporation. The AppleScript described here is not a part of Evernote and is not endorsed or supported by them. 4 | 5 | ### Copyright and License 6 | 7 | Copyright © 2021 William C Jacob Jr 8 | 9 | MIT License 10 | 11 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 16 | 17 | ### Contents 18 | 19 | [toc] 20 | 21 | ### Overview 22 | 23 | The Notebook Export Manager uses Evernote's built-in export features to extract notebooks in HTML or ENEX format and store them in folders of your choice. You can use several selection criteria to determine which notebooks and which notes are included in the export. Most of the selection criteria from Evernote's search syntax can be used to select notes for export. 24 | 25 | ### Installation 26 | 27 | This software is distributed as a single text file containing AppleScript source code. If you've stored it with the default file type of *applescript*, then it will open in the Script Editor app (i.e., AppleScript) when you double-click the file or select it and type command-O (⌘-O). 28 | 29 | Once you've opened the file in the Script Editor, you can edit it and/or run it immediately. To run the script, select Script > Run, type command-R (⌘-R), or click the run icon (►). 30 | 31 | When you first run the script, the Script Editor will convert it to binary format. You can save this binary, along with the source code, in a single script file by exporting it (File > Export) and selecting File Format: Script. The file extension for this combined format is scpt. Most AppleScript users would typically store scripts in this format, as opposed to the text-only file format used to distribute this script. 32 | 33 | When you first run the script, you may be prompted by macOS to permit the script to access the folders where the exported file will be stored. 34 | 35 | ### Settings 36 | 37 | When you first run the script, you will be prompted to choose runtime options, or settings. On subsequent runs, you can always choose to modify settings before running the main export functions of the script. 38 | 39 | Unfortunately, AppleScript does not natively provide the ability to create complex dialog boxes. So each of the customization options is presented in sequence as a separate dialog box. 40 | 41 | As you are stepping through these dialog boxes, you can modify any setting. Your changes are only saved if you step through all the settings. If you cancel at any point, your changes are discarded. 42 | 43 | #### Choose Export Types 44 | 45 | The first setting allows you to choose which export types will be created. The choices are: HTML, ENEX, or both. 46 | 47 | When notebooks are exported as HTML, they will be stored on your computer as HTML files, one per note. Attachments will be stored in subfolders and will be linked from the note HTML file. You can view the note files using only a browser. No other software, including Evernote, is needed to view them, except whatever specialized program, if any, might be needed for attachments. 48 | 49 | The HTML files have been tested, at least basically, in Safari, Chrome, and Firefox. HTML format is useful as archival storage for exported notes, but is not particularly good as a means of transferring notes into another program. The HTML export generated by Evernote is not necessarily visually identical to the note as rendered inside Evernote. Some notes exported as HTML fail to render at all (in Safari). This is an Evernote issue; the script does not alter the contents of exported notes. 50 | 51 | The other export format is ENEX, an Evernote-proprietary, XML-based format. All the notes for a single notebook will be stored in one ENEX file, which includes all the file attachments for each note. 52 | 53 | ENEX files can, in principle, be imported back into Evernote or imported into other note-taking products, such as Apple Notes. If you plan to take advantage of this, you should thoroughly test the import functionality to ensure that note content is faithfully carried forward to the other product. 54 | 55 | #### Select or Create Folder for HTML Exports 56 | 57 | If you've chosen HTML output, you'll be prompted to select a folder where that output is to be stored. Initially, the folder selection dialog will open to your Documents folder. This is probably not a good location for the exports, as they should be placed into a dedicated folder, rather than being intermixed with other folders and files. 58 | 59 | The standard system folder selection dialog box allows you to choose an existing folder or create a new one (using the New Folder button in the lower left). 60 | 61 | This script has been tested with external volumes (e.g., USB thumb drive). If you select an export folder on a removeable volume, then each time the script runs, it will check to be sure the volume is mounted. If not, you'll be prompted to mount it (or to cancel execution of the script). 62 | 63 | #### Select or Create Folder for ENEX Exports 64 | 65 | If you've chosen ENEX output, you'll be asked to choose the output folder for those files. 66 | 67 | The ENEX folder can be a subfolder of the HTML output folder, if you like. Other combinations have not been tested, but will probably work. 68 | 69 | As with the HTML folder, the ENEX folder can be placed on a removeable volume. 70 | 71 | #### Export All Notebooks or only Selected Ones 72 | 73 | You can choose which of your Evernote notebooks will be considered for export. If you choose Selected, the script will show you a list of your notebooks. Hold down the command key (⌘) to select several, if you wish. You must select at least one. 74 | 75 | If you select *all notebooks*, your selection will include notebooks that you created plus notebooks that have been shared with you. 76 | 77 | The first time you run the script, you might want to test it with one or two notebooks, rather than your entire Evernote repository. 78 | 79 | #### Optional Search Terms 80 | 81 | You can limit the notes that will be included in each export by entering optional search terms. By default, this string is blank and all notes will be exported for each selected notebook. 82 | 83 | You can enter any search terms that would be valid in the Search field in the upper right of the Evernote window. More [information](https://help.evernote.com/hc/en-us/articles/208313828-How-to-use-Evernote-s-advanced-search-syntax) is available [online](https://dev.evernote.com/doc/articles/search_grammar.php). However, please **do not** use these terms: 84 | 85 | - `notebook:` 86 | - `updated:` 87 | - `any:` 88 | 89 | The `notebook:` and `updated:` terms are used internally by the script to select notes for export. And the `any:` keyword will "break" the script by sweeping too many notes into each export. (There's a special case relating to use of `updated:`. Please see [Archive Old Notes](#archive-old-notes)) 90 | 91 | Other than those three terms, you should be able to use most of the others. For example, you could specify 92 | 93 | `tag:important` 94 | 95 | to include only notes that are tagged with *important*. Or you could use: 96 | 97 | `-tag:obsolete` 98 | 99 | to exclude notes tagged with *obsolete*. 100 | 101 | Processing of these optional search terms is handled entirely inside Evernote. So, you can test your terms using the search box in the Evernote program. When the script applies your search terms during export processing, those terms will be prefixed with the notebook name and -- except for the first export of a notebook -- the modification date of the notes, for example: 102 | 103 | `notebook:travel updated:20201123T135216Z tag:important` 104 | 105 | For more information about selection by modification date (`updated:`), see the section [Date-Based Export](#date-based-export). 106 | 107 | #### Specify Timeout 108 | 109 | By default, AppleScript limits the run time of a script (that's elapsed time, not processor time). This may not provide enough time to export all your selected notebooks. You can supply a different timeout value, if you wish. The default is 30 minutes. 110 | 111 | In test runs on a Macbook Pro from 2014, it took about 7 to 8 minutes to export 1,530 notes comprising 1.6GB of HTML plus 2.0GB of ENEX files. The export files were written to the internal SSD hard drive. Exports to slower USB thumb drives would likely be slower. 112 | 113 | ### Start Export 114 | 115 | Once you have established your settings -- or immediately, on subsequent executions of the script -- you'll be presented with the choice to Export Now, to Change Settings, or to Cancel. This dialog box also summarizes the current settings for the impending export. 116 | 117 | You can *change settings* repeatedly if you wish. If you have made changes, your settings will be saved and restored the next time you run the script. See Property List File below. 118 | 119 | As the script runs, progress messages will appear as each notebook is processed. When the script completes, an alert message appears showing summary statistics for the export. 120 | 121 | #### Permissions 122 | 123 | The first time you run an export, you may be prompted by macOS for various permissions, depending on whether you've run Script Editor before and what scripts you may have run in the past. For example, you'll be prompted to allow control of System Events, Finder, and Evernote. And you'll be prompted to allow System Events access to the folder (perhaps your Documents) where you've placed the export folders. 124 | 125 | #### Accessing Exported ENEX Files 126 | 127 | Once the export process completes, in the folder you specified there will be one ENEX file for each notebook that was selected. Generally speaking, these files can be re-imported back in to Evernote or imported into other applications. 128 | 129 | #### Accessing Exported HTML Files 130 | 131 | The export process creates a collection of HTML files linked through a set of `index.html` files. They should be viewable in a browser. The top level index page contains a table with one row per exported notebook. It is located a `[your-html-folder]/index.html`. For example, if you choose to set the output folder to `Documents/Evernote Exports`, then the top level index would be at `Documents/Evernote Exports/index.html`. 132 | 133 | You can open this file directly from Finder by double-clicking it or by selecting it and then pressing ⌘-O. You shouldn't move this file because it would break the links to all the underlying notebook indexes. But you can create an alias that can be moved around the file system as you wish. For example, you could create an alias and move it to your desktop. To create an alias, select the top level `index.html` file, then choose File > Make Alias, or right click and choose Make Alias. Move the alias wherever you want. When you open the alias, you'll be opening the top level index. 134 | 135 | Each exported notebook is stored in its own folder which contains an `index.html` file that lists the notes in that notebook. You can open any of these index files or an individual note file directly. 136 | 137 | ### Export Processing 138 | 139 | The script processes notebooks in turn, choosing notes for export and directing Evernote to create the export files and folders. 140 | 141 | #### Notebooks 142 | 143 | Notebooks are processed alphabetically, whether you have selected specific notebooks for export or are exporting all of them. 144 | 145 | For each notebook, the ENEX export takes place followed by the HTML export. After each HTML notebook export, the HTML index page for that notebook is rebuilt. 146 | 147 | If any HTML export was requested or if the top-level HTML index page does not exist, then after all exports have taken place, the top-level HTML index page is rebuilt. There is also an internal switch, `alwaysBuildIndex`, that can be used to force rebuilding of the top level index (by setting the switch to true). 148 | 149 | #### Note Selection 150 | 151 | Notes are selected for export one notebook at a time. All notes will be included unless you've provided [Optional Search Terms](#optional-search-terms). In that case, only notes matching the search terms will be included in the export for that notebook. 152 | 153 | #### Date-Based Export 154 | 155 | After the first export of a notebook in a particular format (i.e., HTML or ENEX), subsequent exports will occur only if the notebook contains changed notes. The script will examine the existing exported files and compare the date when they were exported to the modification dates of notes within the notebook. An export will occur only if Evernote holds notes that have been updated since the previous export. 156 | 157 | For ENEX files, the date of the latest export is stored (by Evernote) in an XML attribute named `export-date` inside the ENEX file. The script reads that attribute to determine when the export was created. 158 | 159 | For HTML files, the date of the last export from a notebook is stored in an HTML comment field within the index.html file for the notebook. The script reads that value. 160 | 161 | The export dates for each individual notebook and for each of the two formats are all stored independently. So, subsequent export choices are made individually for every notebook. This means you can safely export subsets of your Evernote notes without compromising the ability to later export all updated notes. 162 | 163 | ### Known Limitations 164 | 165 | There are some limitations in the operation of this AppleScript, including at least those described here. There are almost certainly other limitations that we are not yet aware of. 166 | 167 | #### Notebook Names with Quotes 168 | 169 | If the name of a notebook includes the quote `"` character, it is subject to special processing. The result should be the same as normal notebooks, but the script uses different logic to select the notes for export. 170 | 171 | This limitation arises from the apparent fact that Evernote search syntax does not seem to allow quotes to be escaped in the search syntax. Although blanks and most special characters can be included in a notebook name (for example,`notebook:"My 😀 ' happy book"`), there doesn't appear to be any way to specify a name with a quote. 172 | 173 | In order to select notes from a notebook containing a quote in its name, the script may have to examine many (or all) notes in Evernote. This may cause it to run very slowly for those notebook(s). 174 | 175 | #### Empty Notebooks 176 | 177 | Empty notebooks in Evernote -- that is, those that contain no notes -- are not processed by this script. 178 | 179 | #### Stacks Not Supported 180 | 181 | The exported notebooks do not include any information about *stacks*. The Evernote interface to Applescript does provide contain any information about whether a notebook is stored within a stack. All exported notebooks are stored together in one directory (one for ENEX and one for HTML). 182 | 183 | #### Evernote Software Version 184 | 185 | This script has been tested with Evernote version 7.14 installed directly from the Evernote website, running on macOS 10.15.7 (Catalina) and macOS 11.1 (Big Sur). It will almost certainly not run with the Evernote application installed from Apple's Mac App Store. 186 | 187 | At this writing, Evernote version 10 is a new and evolving product. At this time -- February 2021 -- version 10 does not support Applescript. 188 | 189 | #### ENEX File Size 190 | 191 | There have been reports online suggesting that Evernote has a limitation on the size of an ENEX file that will be accepted for import. Some say it will not accomodate ENEX files larger than 200MB. So, if you are depending on this script to provide backups for Evernote that you might want to import back into Evernote, you should determine if this limitation does, in fact, exist. 192 | 193 | The same concern might arise if you intend to import these ENEX files into other applications. If you have large notebooks, please test the import functionality before you rely on it. 194 | 195 | #### Duplicate Note Titles 196 | 197 | Apparently, Evernote supports duplicate note titles within a notebook. When this script invokes Evernote to export a notebook, Evernote uses the Note Title to construct the file name (e.g., MyNote.html) for HTML exports. The operating system does not support duplicate file names within a folder. The Evernote export function does not appear to handle this unusual situation and results are unpredictable. 198 | 199 | #### Character Sets 200 | 201 | This script has been written and tested with a locale of en-US using UTF-8. Several unusual notebook names and note titles were used as test cases, with emojis, punctuation and other symbols in the names. They appear to be handled successfully. 202 | 203 | No attempt has been made to test with other language or locale settings. 204 | 205 | The notebook names are used by this script to generate the ENEX export file names and the HTML export folder names. Evernote uses note titles to generate the HTML file names for each exported note. Most unusual characters (e.g., emojis, punctuation, etc) seem to process successfully. The only known exceptions are the forward slash (`/`) and the colon (`:`). Evernote replaces a slash with an underscore and replaces a colon with a slash. 206 | 207 | #### Dates, Timestamps and Time Zones 208 | 209 | Internally, Evernote appears to use GMT date/time values to record notebook creation and modification dates. However, the Evernote app shows dates and times in the user's timezone. 210 | 211 | This script follows that convention. Export date/time stamps in the HTML index files are recorded as GMT times. But for the most part, those times are shown in local time when the user views the index pages. When the user loads an index page, a javascript function updates the displayed dates and times to match the user's local time zone. 212 | 213 | At least, that's how it is designed to work. However, it has only been tested in one time zone (US Eastern). So, further testing, in multiple time zones, would be helpful. 214 | 215 | #### Serialization 216 | 217 | This script makes no attempt to serialize access to the Evernote data store during export operations. In other words, if someone else is updating a note while the export is taking place or if the Evernote app on your computer is syncing with the online repository, results are unpredictable. 218 | 219 | Accordingly, it is good practice to click the Evernote sync button before running an export, to help ensure that the export gets the most recent version of each note. 220 | 221 | #### HTML Format 222 | 223 | When notes are exported by Evernote, there are some compromises in formatting. Not all exported notes appear the same as when viewed in Evernote. Indeed, some notes created from clipped web pages do not display at all in Safari. These exported HTML files are created directly from Evernote. The script has no role in determining the HTML content. 224 | 225 | If visual fidelity is important to you, please take a careful look at the exported versions. 226 | 227 | #### Evernote Export Limit 228 | 229 | There are comments online about an export limit of 50 notes. This limit is apparently imposed on free Evernote accounts. I have a paid account, so I don't know if the limit applies to exports triggered via AppleScript calls to Evernote. In other words, I don't know how the script will behave when accessing a free Evernote account. 230 | 231 | ### Known Errors 232 | 233 | #### Evernote Got An Error 234 | 235 | On rare occasions, Evernote is unable to export a notebook in HTML format due to some internal problem with the resources (i.e., attachments) for a particular note. This manifests in the Notebook Export Manager script as a failure in line 254 of the script, inside the `exportNotes` handler (a few lines from the end of that handler): 236 | 237 | ``` 238 | if oType = "HTML" then tell application "Evernote" to export selectedNotes to oFile format HTML 239 | ``` 240 | 241 | The reported error is too vague ("Evernote got an error") to narrow the problem down to a particular note. No further information is returned to AppleScript to allow it to be diagnosed by the script. The recommended recovery is to go back to Evernote and manually attempt to export the offending notebook: 242 | 243 | 1. Look at the export output directory for HTML. It will contain some notebooks, but not all. The notebook that caused the failure, and all those after it (in alphabetical order) will be missing. If you were exporting ENEX files at the same time, you'll find a successful ENEX export file, but not an HTML output folder for the failing notebook. 244 | 2. Go to Evernote. Click on View > Notebooks (or ⌥⌘1). Scroll down to the failing notebook, right-click (or click the three dots to the right) and select *Export Notes from "notebook"* and choose HTML format. 245 | 3. Evernote will begin processing the notebook for HTML export and will stop on the note that's causing the problem. 246 | 4. Manually resolve the problem by deleting the offending note or copying and pasting its content into a new note (then delete the offending note). It's not obvious why these few notes fail. The attachments seem to be accessible within the Evernote app, but they fail during export. 247 | 5. Retry the export from Notebook Export Manager. Typically, you do not need to reset the Settings to start with the previously failed notebook. The script will skip past the notebooks that were successfully exported at the rate of about 1 per second. 248 | 249 | In a full export of about 2,500 notes across about 60 notebooks, three notes had this problem. Once they were resolved, the export completed successfully. 250 | 251 | ### Use Cases 252 | 253 | This section discusses considerations for using this script. 254 | 255 | #### Backup 256 | 257 | This script could be used to create a backup, on your computer, of data stored in Evernote. However, the script does not keep multiple versions of notes as they are changed in Evernote. Only the most recently exported version is available in the HTML or ENEX files. 258 | 259 | The script makes no attempt to synchronize changes made in Evernote with the HTML and ENEX output files, except to export notebooks that contain newly changed notes. Other changes are not reflected in the exported files. Here are some examples of changes that are not handled by the script: 260 | 261 | - Changing tags: If you change the tags assigned to a note, it does not appear that the modification date for the note gets updated. In other words, the note appears to be unchanged. This script does not detect the tag change and thus will not re-export the note or update its tag listings in the HTML export files. 262 | - Deleting notes: When Evernote is called to export a notebook, the target location is first erased. For an ENEX export, the target `.enex` file is overwritten. For an HTML export, the target folder for the notebook is emptied before the export takes place. So, if a note is exported and is later deleted in Evernote, following a later export, it will no longer exist within the exported copies. However, the deletion of a note -- by itself -- will not trigger an export. Only a change to some other note in the same notebook would trigger an export. 263 | 264 | Depending on your folder configuration, exported ENEX and HTML notebooks could be included in your local backup solution, such as Time Machine or an internet-based backup service. As noted elsewhere in this document, you can also direct the script to export to a removeable device, such as a USB thumb drive. 265 | 266 | #### Archive Old Notes 267 | 268 | This script could be used to archive older notes, using a special [Optional Search Term](#optional-search-term). It can be used to export only those notes updated before a user-specified date. This techique will work only the first time a particular notebook is exported into a folder. 269 | 270 | The user would change the settings and set an Optional Search Term such as `-updated:20180101`, for example (note the leading dash). This would restrict the export to notes not updated since January 1, 2018. 271 | 272 | This technique only works the first time a notebook is exported into a particular folder. In subsequent exports into the same folder, the date-based export will find the previously exported search and insert a updated:[date] term into the search string. This will cause a conflict in the search, with unpredictable results. So, if you create an archive of old notes, you should probably take steps to prevent those notebook(s) from being re-exported into the same HTML or ENEX files and folders. 273 | 274 | This archiving technique could use additional search terms. For example: `-updated:20170401 tag:history` would select notes not updated since April 2017 that are tagged `history`. You can test your optional search strings using the search box in the Evernote app. 275 | 276 | #### Export for Web 277 | 278 | The notebooks and notes exported as HTML have been cursorily tested with a browser. It's possible that this script could be used to generate HTML folders that could be placed on a web server on an intranet or the Internet. 279 | 280 | ### Exported Folders and Files 281 | 282 | This section describes the folders and files created by the script. 283 | 284 | #### ENEX 285 | 286 | Within the ENEX export folder selected by the user, this script creates one ENEX file for each exported notebook. The script invokes Evernote to create each file, using the notebook name to derive the file name. The internal format of these files is set solely by Evernote. 287 | 288 | #### HTML Folder Structure 289 | 290 | Within the HTML export folder selected by the user, this script creates several files in the top folder and creates a subfolder for each exported notebook. 291 | 292 | These files are created in the top level folder: 293 | 294 | - `index.html` is the top level web page for all exported notebooks. It can be opened in your default browser (double-click or select and ⌘-O). This file is regenerated on each run of the script, so long as at least one HTML export occurred or if it does not already exist. 295 | - `entags.html` is the top level list of tags found in all exported notes. It is linked from `index.html` and from the index pages for every exported notebook. 296 | - `enscripts.js` contains the javascript that powers initialization and sorting functionality on all the index pages. This javascript file is created by the script the first time it runs. The source for the file is stored in the script as a text constant. If the script finds this file already in place, it does not overwrite it. So, if the user chooses to edit it, the changes will not be overwritten. 297 | - `enstyles.css` contains CSS to format the index pages created by the script. As with the javascript file, this CSS file is created on the first run of the script and will not subsequently be overwritten. 298 | 299 | #### Notebook Subfolders 300 | 301 | Each exported notebook is placed in its own subfolder, which is created by Evernote when that notebook is first exported. A notebook subfolder contains: 302 | 303 | - An `index.html` file, originally created by Evernote during the notebook export, and then replaced with a more functional version by this script, after Evernote has completed the export. This index file contains a link to every exported note and a table of tags used within the notebook. 304 | - An html file for each exported note, with a file name derived from the note title. 305 | - If a note contains file attachments, they are exported into a lower level folder unique to that note. 306 | 307 | #### Notebook Index Pages 308 | 309 | When Evernote is called to export a notebook as HTML, it creates a very simple index.html page for the notebook, providing links to each exported note. This script replaces that index page with a more complete index showing note titles, creation and modification dates, size (on disk), number of attached files, author, and associated tags. The header in each colunn can be clicked to sort the table. 310 | 311 | Each notebook index page also contains a sortable table showing any tags assigned to the notes in that notebook. 312 | 313 | Embedded within HTML comments in the notebook index page is the following information: 314 | 315 | - the date and time this notebook was last exported from Evernote 316 | - some descriptive information about the environment when the export occurred, such as the version of Evernote, the locale (e.g, en-US), etc. 317 | - the data (one table row) needed to create this notebook's entry in the top-level index. 318 | - the tag section, if any, of the notebook index page, which is wrapped with comments that allow it to be extracted and added to the top-level tag page. 319 | 320 | If these comments are altered, the script probably will not function correctly. 321 | 322 | Most fields in the table are self-explanatory. The date fields will show a tooltip with the date and time of day. If the Author field included an email address, that address is displayed as a tooltip and the display author name includes an envelope icon (✉️). 323 | 324 | The notebook index page is rebuilt every time the notebook is exported as HTML. 325 | 326 | #### Top Level Index Page 327 | 328 | The top level `index.html` file contains a web page with links to each exported notebook index page. This page is rebuilt whenever an export operation results in an HTML export or if the user has set the `alwaysBuildIndex` to `true` in the script. The page is built entirely by extracting hidden comment fields from the individual notebook index pages. So, it reflects the contents of the exported file, rather than the live contents in the Evernote repository. 329 | 330 | All exported notebooks will be reflected in the top level index page, even if they were not exported in the current run of the script. For example, assume you export all 10 of your notebooks. The next day, you run the script again, but you change the settings to only export 2 notebooks. When the top level index is rebuilt during that second run, it will reflect the two notebooks just exported plus the other 8 that are still present on disk from the day before. 331 | 332 | #### Javascript file 333 | 334 | The first time a notebook is exported as HTML, the script creates a file, `enscript.js`, containing javascript that is invoked on each notebook index page and on the top-level index and tag pages. The code for this file originates from a text constant within the script. This method of distribution was chosen so that the user doesn't have to place the file in the output directory; it happens automatically. 335 | 336 | On subsequent executions of the script, the file is not overwritten. So, if the user makes any changes to the javascript, those changes remain in place. 337 | 338 | The javascript file contains several functions that are invoked on the index pages: 339 | 340 | - setLocalDates is used to adjust displayed dates to the user's locale. Dates, such as the modification date of a note, are stored as GMT values in the data-gmtdate attribute of tags. This script, called via the onload event, calculates the local date and updates the displayed value for the table cell and a title for each cell showing local date and time (i.e., a tool-tip). 341 | - sortTable is used to sort tables when a user clicks on a table heading cell. 342 | - hideNBnames hides selected columns in a table by dynamically creating a specified class with the `display:none` attribute. This is used because the same basic Tag table appears in each notebook index page and, in aggregate form, in the top-level tag index page. 343 | - settarget implements the *open in new tab* functionality, as controlled by the checkbox in the upper right corner of each index page. 344 | 345 | #### CSS File 346 | 347 | The `enstyles.css` file is created on the first run of the script to provide formatting instructions for the index pages. Changes to this file will be reflected in the top-level pages (index and tags) and in each notebook index page. Like the javascript file, the CSS file is not overwritten by the script after it's first created. 348 | 349 | #### Moving or Renaming 350 | 351 | If you move the HTML folder structure, all the internal links on the index.html pages should continue to work, because they are relative to the top-level index page. 352 | 353 | However, renaming subfolders or html pages is more complicated and will probably create useability problems. 354 | 355 | ### Customization 356 | 357 | #### Property List File 358 | 359 | The script stores its settings in a standard macOS Property List File or plist. The name and full path to this file are stored in a variable named `myPListFullPath` early in the script. 360 | 361 | By convention, the plist files are stored in `~/Library/Preferences`. The filename is also set in the script, on the same line as the path. The user could alter the name or location, if desired. 362 | 363 | #### Rebuild Top Index 364 | 365 | The top-level HTML index page is rebuilt in these situations: 366 | 367 | - If it does not exist 368 | - If any notebook has been exported in HTML format 369 | - If the variable `alwaysBuildIndex` is set to `true`. It is set that way in the current version of the script. To suppress this behavior, set it to `false`. 370 | 371 | #### Rebuild Notebook Indexes 372 | 373 | Normally, the `index.html` page is build only when a notebook is exported in HTML format. However, there is a switch in the script that will force these pages to be rebuilt for any notebook examined during the run. This might be useful if the content of these index pages needs to be updated, but it is otherwise not necessary to re-export the notebooks from Evernote. 374 | 375 | To force rebuilding, set the variable `alwaysBuildBookIndexes` to `true`. The index for every notebook or for the notebooks you've selected will be rebuilt even if the script does not find it necessary to re-export that notebook. 376 | 377 | In this mode, if an index.html file is being rebuilt for a notebook that was not just exported, the script will read the existing index and retain the Export Date from the previous export. 378 | 379 | #### Diagnostic Logging 380 | 381 | A switch named `logProgress` can be set to `true` to cause the script to log various diagnostic information to the Script Editor log during execution. 382 | -------------------------------------------------------------------------------- /Test_Top.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BitRancher/notebook-export-manager/0beb6f1df9083cffb2c6a3127650c7496d0203b3/Test_Top.png -------------------------------------------------------------------------------- /Test_notebook.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BitRancher/notebook-export-manager/0beb6f1df9083cffb2c6a3127650c7496d0203b3/Test_notebook.png -------------------------------------------------------------------------------- /Test_tags.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BitRancher/notebook-export-manager/0beb6f1df9083cffb2c6a3127650c7496d0203b3/Test_tags.png --------------------------------------------------------------------------------