Edit: Updated scripts - Support for bibtex added; Compile only selected TeX file; Run compilation for all available TeXf files in group if group or some other file (not TeX file) is selected
I prefer using LaTeX for (longer) documents and business letters. For years, I stored my TeX files in git repositories in the filesystem and use Makefile’s to generate the PDF files. After moving to MacOS and DEVONthink I moved all generated PDFs to DT. I left the “source” TeX in the filesyste. With versions available within DT4, I thought to move all files to DT.
Benefits of using this script
With this script everything is within DT - TeX source(s) and the generated PDFs - BUT: it requires MacTex to be installed!
The script
Although it has the structure of a performSmartRulescript , I use it as a script in “Contextual Menu” within DT, after suggestions of @cgrunenberg somewhere else.
Primary focus for now is generating letters, the script might need some adjustments to run complex documents with includes.
scripts.zip (43.2 KB)
use AppleScript version "2.4" -- Yosemite (10.10) or later
use scripting additions
use framework "Foundation"
use script "RegexAndStuffLib" version "1.0.7"
property globalSuccessTags : {"auto-processed-pdflatex-generate"}
property globalAppendixName : "Anhänge"
property globalAppendixPrefix : "inkl"
property globalOutputName : "Fertige Dokumente"
property globalTmpName : "Temp"
tell application id "DNtp" to my performSmartRule(selected records)
-----------
-- test performSmartRule
property globalIsDev : true
-- copy template for smart rules
on performSmartRule(theRecords)
local libraryHandler
set libraryHandler to script "library-handling"
tell application id "DNtp"
set currentDb to current database
end tell
libraryHandler's canRun(currentDb, theRecords)
local scriptName
set scriptName to "pdflatex"
local theScriptHandler
set theScriptHandler to libraryHandler's loadScript(scriptName, globalIsDev)
theScriptHandler's dtLog(missing value, "INFO", "Run script", {"script=" & quoted form of scriptName})
theScriptHandler's ForAll(theRecords, globalIsDev)
theScriptHandler's dtLog(missing value, "INFO", "Script finished", {"script=" & quoted form of scriptName})
end performSmartRule
-- Run job for all given records
on ForAll(theRecords, localIsDev)
-- aggregate tex files over all groups into array
local theTexFileRecords
set theTexFileRecords to {}
repeat with theRecord in theRecords
local theGroup
tell application id "DNtp"
set theGroup to my getTheGroup(theRecord)
end tell
local foundTexFileRecords
tell application id "DNtp"
set foundTexFileRecords to my getTexFiles(theRecord, theGroup)
end tell
repeat with foundTexFileRecord in foundTexFileRecords
set theTexFileRecords to theTexFileRecords & {{r:foundTexFileRecord, g:theGroup}}
end repeat
end repeat
local titleProgressBar
set titleProgressBar to "Generate PDF documents for groups"
tell application id "DNtp"
show progress indicator titleProgressBar steps (count of theTexFileRecords) with cancel button
end tell
-- https://developer.apple.com/library/archive/documentation/AppleScript/Conceptual/AppleScriptLangGuide/reference/ASLR_error_xmpls.html
set len to length of theTexFileRecords
set i to 1
my dtLog(missing value, "INFO", quoted form of "Job started", {"count_of_records=" & len})
local firstGroup
tell application id "DNtp"
set firstGroup to my getTheGroup(item 1 of theRecords)
end tell
local outputGroup
tell application id "DNtp"
set outputGroup to create location globalOutputName in firstGroup
delete record outputGroup in firstGroup
set outputGroup to create location globalOutputName in firstGroup
end tell
try
local theTexFileRecord
repeat with theTexFileRecord in theTexFileRecords
-- re-set variable to match normal setup
set theRecord to r of theTexFileRecord
set theGroup to g of theTexFileRecord
tell application id "DNtp"
if cancelled progress then
my dtLog(theRecord, "INFO", quoted form of "Cancel current run of script", {})
exit repeat
end if
end tell
tell application id "DNtp"
set theFilename to the filename of theRecord
set theGroupname to the name of theGroup
step progress indicator ((i as rich text) & "/" & len as rich text) & ": " & my joinPath({theGroupname, theFilename})
end tell
my dtLog(theRecord, "INFO", quoted form of "Work on group", {"index=" & i & "/" & len})
my ForOne(theRecord, theGroup, outputGroup, localIsDev)
set i to i + 1
end repeat
on error errMsg number errNum partial result partialError
tell application id "DNtp"
hide progress indicator
end tell
if errNum = 1000 then
my dtLog(missing value, "WARN", quoted form of "Job execution aborted by user", {"error=" & errNum, "count_of_records=" & len})
return
end if
set AppleScript's text item delimiters to {return}
-- An unknown error occurred. Resignal, so the caller
-- can handle it, or AppleScript can display the number
display alert errMsg & ("Error number: ") & errNum & return & (partialError as text)
error errMsg & ("Error number: ") & errNum & return & (partialError as text)
end try
tell application id "DNtp"
hide progress indicator
end tell
my dtLog(missing value, "INFO", quoted form of "Job completed", {"count_of_records=" & len})
end ForAll
-- Run job for single record
on ForOne(theRecord, theGroup, outputGroup, localIsDev)
-- generat PDF from LaTeX file
local pdfPath
try
set pdfPath to generatePDF(theRecord, theGroup, outputGroup)
on error errMsg number errNum partial result partialError
if errNum = 2000 then
my dtLog(theRecord, "WARN", quoted form of "Job failed for record. Moving to next one.", {"error=" & errNum, "error_msg=" & errMsg})
return missing value
end if
set AppleScript's text item delimiters to {return}
-- An unknown error occurred. Resignal, so the caller
-- can handle it, or AppleScript can display the number
error errMsg & ("Error number: ") & errNum & return & (partialError as text)
end try
local generatedPdfRecord
set generatedPdfRecord to addPdfToDt(theRecord, outputGroup, pdfPath)
addAppendicesToPdf(theRecord, outputGroup, generatedPdfRecord, localIsDev)
end ForOne
on getTheGroup(theRecord)
local theGroup
set theGroup to theRecord
tell application id "DNtp"
if record type of theRecord is not group then
-- Get the parent group of the original record
local theGroup
set theGroup to location group of theRecord
my dtLog(theGroup, "WARN", quoted form of "Record is not a group - using parent location instead", {"record-type=" & record type of theRecord, "new-location=" & (location with name of theRecord)})
end if
end tell
return theGroup
end getTheGroup
on getTexFiles(theRecord, theGroup)
local foundTexFileRecords
tell application id "DNtp"
if kind of theRecord is "TeX" then
set foundTexFileRecords to {theRecord}
else
set foundTexFileRecords to (search "extension==tex name:!<_ name:!<- name:!<." in theGroup)
end if
end tell
return foundTexFileRecords
end getTexFiles
on addPdfToDt(theRecord, outputGroup, pdfPath)
local newRecord
tell application id "DNtp"
set newRecord to import path pdfPath to outputGroup
end tell
local newRecordLocation
tell application id "DNtp"
set newRecordLocation to location with name of newRecord
end tell
my dtLog(newRecord, "DEBUG", quoted form of "Import PDF file into database", {"path=" & quoted form of pdfPath, "location-new-record=" & newRecordLocation})
return newRecord
end addPdfToDt
on addAppendicesToPdf(theRecord, outputGroup, generatedPdfRecord, localIsDev)
local recordGroup
tell application id "DNtp"
set recordGroup to (location group of theRecord)
end tell
-- set location to add merged pdf to
local appendixGroup
tell application id "DNtp"
set appendixGroup to create location globalAppendixName in recordGroup
end tell
local tmpGroup
tell application id "DNtp"
set tmpGroup to create location globalTmpName in recordGroup
delete record tmpGroup in recordGroup
set tmpGroup to create location globalTmpName in recordGroup
end tell
-- search for appendices
local appendicesPdfRecords
tell application id "DNtp"
set appendicesPdfRecords to (search "type:pdf" in appendixGroup)
end tell
-- nothing to merge
if length of appendicesPdfRecords is 0 then
return missing value
end if
local modifiedRecords
set modifiedRecords to {}
tell application id "DNtp"
repeat with appendicesPdfRecord in appendicesPdfRecords
local headlineMarkdownRecord
set headlineMarkdownRecord to create record with {name:name without extension of appendicesPdfRecord & "_h1", content:name without extension of appendicesPdfRecord & return & return & comment of appendicesPdfRecord as rich text, record type:markdown} in tmpGroup
local headlinePdfRecord
set headlinePdfRecord to convert record headlineMarkdownRecord to PDF document in tmpGroup
local duplicateRecord
set duplicateRecord to duplicate record appendicesPdfRecord to tmpGroup
set name of duplicateRecord to (name of duplicateRecord) & "_content"
set mergedRecord to merge records {headlinePdfRecord, duplicateRecord} in tmpGroup
copy mergedRecord to end of modifiedRecords
end repeat
end tell
-- add/merge appendices to/with PDF file
-- merge is missing value on single pdf input
local allFilesRecord
tell application id "DNtp"
set allFilesRecord to merge records {generatedPdfRecord} & modifiedRecords in outputGroup
end tell
tell application id "DNtp"
set name of allFilesRecord to my joinWithDash({name without extension of theRecord, globalAppendixPrefix, globalAppendixName})
end tell
local groupLocation
tell application id "DNtp"
set groupLocation to location with name of outputGroup
end tell
local allFilesLocation
tell application id "DNtp"
set allFilesLocation to location with name of allFilesRecord
end tell
local cntAppendices
set cntAppendices to length of appendicesPdfRecords
my dtLog(generatedPdfRecord, "INFO", quoted form of "Created merged PDF", {"merged-pdf-location=" & allFilesLocation, "group-location=" & groupLocation}, "count-merged-appendices=" & cntAppendices)
if localIsDev is false then
tell application id "DNtp"
delete record tmpGroup in recordGroup
end tell
end if
end addAppendicesToPdf
on getRelativeLocation(theRecord, theGroup)
local theParent
tell application id "DNtp"
set theParent to location group of theRecord
end tell
if theParent is missing value then
my dtLog(theGroup, "DEBUG", quoted form of "Reached database root", missing value)
return
end if
local parentLocation
tell application id "DNtp"
set parentLocation to location with name of theParent
end tell
local groupLocation
tell application id "DNtp"
set groupLocation to location with name of theGroup
end tell
if parentLocation is groupLocation then
my dtLog(theGroup, "DEBUG", quoted form of "Compare locations for the group and the base group", {"base-group-location=" & parentLocation, "group-location=" & groupLocation & "result=same"})
return {}
end if
local locationName
tell application id "DNtp"
set locationName to name of theParent
end tell
local newLocations
set newLocations to my getRelativeLocation(theParent, theGroup)
return newLocations & {locationName}
end getRelativeLocation
on generatePDF(theTexFileRecord, theGroup, outputGroup)
local texFileName
tell application id "DNtp"
set texFileName to proposed filename of theTexFileRecord as rich text
end tell
local texFileNameNoExt
tell application id "DNtp"
set texFileNameNoExt to name without extension of theTexFileRecord as rich text
end tell
local relativeLocationList
set relativeLocationList to my getRelativeLocation(theTexFileRecord, theGroup)
local relativeLocation
set relativeLocation to my joinPath(relativeLocationList)
my dtLog(theGroup, "DEBUG", quoted form of "Gather relative location for TeX file", {"tex-file=" & texFileName, "relative-location=" & relativeLocation})
-- Create unique export destination using record's UUID
local mkTempDirCmd
set mkTempDirCmd to "mktemp -d"
local tempDir
set tempDir to do shell script mkTempDirCmd
local exportedPath
tell application id "DNtp"
-- Export the group
my dtLog(theGroup, "DEBUG", quoted form of "Export record to temporary folder", {"directory=" & quoted form of tempDir})
set exportedPath to export record theGroup to tempDir
end tell
local groupLocation
tell application id "DNtp"
set groupLocation to location with name of theGroup
end tell
-- If export was successful, run pdflatex in that directory
if exportedPath is missing value then
tell application id "DNtp"
my dtLog(theGroup, "WARN", quoted form of "Export failed for group", {"group=" & quoted form of groupLocation, "export-path=" & exportedPath})
end tell
tell application id "DNtp"
display alert "Export failed for group for " & groupLocation & " failed. See logs for full log trace"
end tell
error "Group export failed" number 2000
end if
local workingDirectory
set workingDirectory to my joinPath({exportedPath, relativeLocation})
local pdfCmd
set pdfCmd to generateCmdString(workingDirectory, texFileName, texFileNameNoExt)
my dtLog(theGroup, "DEBUG", quoted form of "Run pdf generator command", {"cmd=" & quoted form of pdfCmd})
try
do shell script pdfCmd
on error errMsg number errNum partial result partialError
my dtLog(theTexFileRecord, "ERROR", quoted form of "Generating PDF failed", {"filename=" & texFileName, "export-directory=" & quoted form of exportedPath, "error_msg=" & quoted form of errMsg & quoted form of (partialError as text), "error_number=" & errNum})
local logFileName
tell application id "DNtp"
set logFileName to (name without extension of theTexFileRecord as rich text) & ".log"
end tell
local logPath
tell application id "DNtp"
set logPath to my joinPath({exportedPath, relativeLocation, logFileName})
end tell
tell application id "DNtp"
display alert "Generating PDF for " & texFileName & " failed. See DEVONthink and pdflatex logs for full log trace."
end tell
local logExists
set logExists to (do shell script "test -f " & quoted form of logPath & " && echo 'yes' || echo 'no'") is "yes"
my dtLog(theGroup, "DEBUG", quoted form of "Test for existence of log file for PDF generation", {"path=" & quoted form of logPath, "result=" & logExists as text})
if logExists then
my dtLog(outputGroup, "WARN", quoted form of "Adding pdflatex logs to DEVONthink", {"path=" & quoted form of logPath})
local logRecord
tell application id "DNtp"
set logRecord to import path logPath to outputGroup
set name of logRecord to (name of logRecord) & "-pdflatex-error"
end tell
-- set selection for scripts
tell application id "DNtp"
local theWindow
set theWindow to think window 1
set selection of theWindow to {logRecord}
end tell
end if
error "PDF generation failed" number 2000
end try
local pdfFileName
tell application id "DNtp"
set pdfFileName to (name without extension of theTexFileRecord as rich text) & ".pdf"
end tell
local texFileLocation
tell application id "DNtp"
set texFileLocation to location group of theTexFileRecord
end tell
local pdfPath
tell application id "DNtp"
set pdfPath to my joinPath({exportedPath, relativeLocation, pdfFileName})
end tell
local pdfExists
set pdfExists to (do shell script "test -f " & quoted form of pdfPath & " && echo 'yes' || echo 'no'") is "yes"
my dtLog(theGroup, "DEBUG", quoted form of "Test for existence of PDF file", {"path=" & quoted form of pdfPath, "result=" & pdfExists as text})
if not pdfExists then
error "PDF generation failed" number 2000
end if
return pdfPath
end generatePDF
on generateCmdString(workingDirectory, theFilename, theFilenameNoExt)
local cmd
set cmd to "cd " & quoted form of workingDirectory & " && /Library/TeX/texbin/pdflatex " & quoted form of theFilename & " && (/Library/TeX/texbin/bibtex " & quoted form of theFilenameNoExt & " || exit 0) && /Library/TeX/texbin/pdflatex " & quoted form of theFilename & " && /Library/TeX/texbin/pdflatex " & quoted form of theFilename
return cmd
end generateCmdString
on dtLog(theRecord, level, msg, msgInfo)
if class of msgInfo is not list and msgInfo is not missing value then
error "Invalid definition of msgInfo in dtLog: Needs to be {}"
end if
if msgInfo is missing value then
set logMsg to "msg=" & msg
else
set logMsg to "msg=" & msg & " " & (join strings msgInfo using delimiter return)
end if
if level is not "DEBUG" then
tell application id "DNtp"
if theRecord is missing value then
log message "level=INFO " & logMsg
else
log message record theRecord info "level=INFO " & logMsg
end if
end tell
return
end if
if globalIsDev is true then
tell application id "DNtp"
if theRecord is missing value then
log message "level=INFO " & logMsg
else
log message record theRecord info "level=INFO " & logMsg
end if
end tell
end if
end dtLog
on joinPath(elements)
return join strings elements using delimiter "/"
end joinPath
on joinWithDash(elements)
return join strings elements using delimiter "-"
end joinWithDash
Setup
-
Install TeX Live
brew install --cask mactex-no-gui bibdesk -
Create scripts - pdflatex.scpt and library-handling.scpt (or replace/remove code from library-handling.scpt) in ~/Library/Application Scripts/com.devon-technologies.think/Contextual Menu
-
Create template/example group in DT
template.zip (27.1 KB)
-
Select test.tex and run pdflatex script
-
Change global variables matching your needs
property globalSuccessTags : {“auto-processed-pdflatex-generate”}
property globalAppendixName : “Anhänge”
property globalAppendixPrefix : “inkl”
property globalOutputName : “Fertige Dokumente”
property globalTmpName : “Temp”
How it works
- It search for TeX files in group(s) if group is selected or a non-TeX file or compiles selected TeX file only
- It exports groups to filesystem
- It generates PDF with
pdflatex; bibtex; pdflatex; pdflatex - It ignores TeX files prefixed with an underscore - assumption: included files
- It adds PDF file to DT
- It adds additional files to the document if stored at a predefined place - globalAppendixName
- Use template.zip (27.1 KB) as an example to see how it works
- Errors etc. are logged to DT
- Log of pdflatex is added to output directory on error + current selection is set to pdflatex-log file
- Files beginning with _ or - are ignored while searching for TeX-files if a group is selected

