Skip to content

Instantly share code, notes, and snippets.

@crmackay
Created June 24, 2016 00:30
Show Gist options
  • Save crmackay/0b513022708c3dbad8b699105b9db8e1 to your computer and use it in GitHub Desktop.
Save crmackay/0b513022708c3dbad8b699105b9db8e1 to your computer and use it in GitHub Desktop.
latex build script (work in progress)
"""
markdown --> pandoc --> latex --> pdf builder script
features:
- checkes for changes in the current working directory
- stores change data between runs in this file itself (at the bottom)
- will only rebuild and rerun those parts that need to be rebuilt
- will run `pdflatex` only as many times as necessary
- for every `.md` file in the cwd it will run pandoc and produce a so-named `.tex` latex file
- if any `.tex` file changes then pdflatex is run
- pdflatex will then write to .aux, .log, and .out files, and changes to these files
trigger pdflatex to run again
- pdflatex is run until no more changes to these files are seen (ie it's done)
todo:
- add bibtex support
"""
import os, hashlib, subprocess, sys
def storedHashes():
""" returns a dictionary of stored paths and stored hashes
"""
with open("build.py", "r") as dataFile:
data = {}
withinData = False
for line in dataFile:
if line.startswith("# END DATA"):
withinData = False
if withinData == True:
filename, hash = line.lstrip("# ").rstrip("\n").split("\t")
data[filename] = hash
if line.startswith("# BEGIN DATA"):
withinData = True
#data will be empty if no data or data fields are present
return data
def saveHashes(data):
""" saves the current hashes to the `build.py` file found in the cwd
"""
previouslySaved = False
file = ""
toWrite = ""
with open("build.py","rw") as dataFile:
file = dataFile.read()
fileLines = file.split("\n")
for i, line in enumerate(fileLines):
if line.startswith("# BEGIN DATA"):
previouslySaved = True
if previouslySaved == True:
toWrite = replaceData(fileLines, data)
else:
toWrite = saveFresh(fileLines, data)
with open("build.py","w") as dataFile:
dataFile.write("\n".join(toWrite))
return
def replaceData(listOfLines, data):
""" used to find and replace hash data stored in `build.py` with new data
"""
start = 0
end = 0
for i, line in enumerate(listOfLines):
if line.startswith("# BEGIN DATA"):
start = i
elif line.startswith("# END DATA"):
end = i
forInsertion = []
for k,v in data.iteritems():
forInsertion.append("# "+"\t".join([k,v]))
return listOfLines[0:start+1]+forInsertion+listOfLines[end:]
def saveFresh(listOfLines, data):
""" used if no previous data is present in the `build.py` file, to store said data
"""
forInsertion = []
forInsertion.append("")
forInsertion.append("# BEGIN DATA")
for k,v in data.iteritems():
forInsertion.append("# "+"\t".join([k,v]))
forInsertion.append("# END DATA")
return listOfLines + forInsertion
def findChanges(old, new):
""" given two dictionaries of paths and hashes, this function crossreferences each one
with the other looking for differences. Files found in old, but not in new are ignored.
Files found in new, but not in new are deemed "changed". All other files are examined by
old and new hash to determine is any changes have occured
"""
changedFiles = []
for file, old_hash in old.iteritems():
if file in new:
new_hash = new[file]
if old_hash != new_hash:
changedFiles.append(file)
for file, new_hash in new.iteritems():
if file not in old:
changedFiles.append(file)
print changedFiles
return changedFiles
def changeHandler(extension):
""" returns the handler function for a given file extenion
"""
if extension == ".md":
return handleMarkdown
elif extension == ".tex" or extension ==".log" or extension == ".aux" or extension == ".out":
return handleLatex
else:
return handleDefault
def handleMarkdown(file, verbose):
""" handles markdown files, converting them to latex
"""
mdown = ''
with open(file, "r") as inFile:
mdown = inFile.read()
inFile = ''
inStart = 0
inEnd = 0
if "\input{" in mdown:
# print "found input"
inStart = mdown.find("\input{")
done = False
inFile = ''
i = 0
while done == False:
# print "parsing name"
next = mdown[inStart+7+i]
if next == "}":
done = True
inEnd = inStart+7+i
else:
i+=1
inFile += next
# print("filename: ", inFile)
if inFile.endswith(".md"):
# print "detected as markdown"
forInput = ''
with open(inFile, "r") as inFile:
forInput = inFile.read()
mdown = mdown[:inStart] + forInput + mdown[inEnd:]
command = ["pandoc", " -o" + file.rstrip(".md") + ".tex", " -f markdown+yaml_metadata_block "]
# command = ["pandoc", " -s -o main.tex", " -f markdown+yaml_metadata_block "]
if file == "main.md":
command = command[0:1]+["-s"]+command[1:]
p = subprocess.Popen(" ".join(command), shell=True, stdin = subprocess.PIPE, stdout=subprocess.PIPE)
stdout_data = p.communicate(input=mdown)[0]
print "handling markdown"
if verbose:
print stdout_data
def handleLatex(file, verbose):
output = subprocess.Popen("pdflatex -interaction=nonstopmode main.tex", shell=True, stdout=subprocess.PIPE).stdout.read()
print "handling latex"
if verbose:
print output
def handleDefault(file, verbose):
print "handling default"
def getChanges(dir):
old = storedHashes()
new = currentHashes(dir)
changes = findChanges(old, new)
saveHashes(new)
return changes
def currentHashes(dir):
filesToWatch = []
for path in os.listdir(dir):
if path != "build.py" and os.path.isfile(path):
filesToWatch.append(path)
results = {}
for file in filesToWatch:
results[file] = hashlib.sha256(open(file, 'rb').read()).hexdigest()
return results
if __name__ == "__main__":
verbose = False
if len(sys.argv) > 1:
if sys.argv[1] == "-v":
verbose = True
else:
print ("to print verbose error messages, use '-v'")
cwd = os.getcwd()
# TODO: handle each file seperatley and check for changes in the handler
# - this allows for markdown handlers to check for \input{file.md} changes
changes = getChanges(cwd)
while len(changes) > 0:
print changes
for file in changes:
if file == "build.py":
continue
_, extension=os.path.splitext(file)
print extension
changeHandler(extension)(file, verbose)
changes = getChanges(cwd)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment