From f2eb4075503ae5277edcbaadb6f21756498072dd Mon Sep 17 00:00:00 2001 From: Jeremy Manning Date: Sun, 14 Dec 2025 06:09:57 -0500 Subject: [PATCH 1/4] Add CV build system: LaTeX parser and build script --- documents/JRM_CV.html | 386 ++++++++++++++++++++++++++++++++++ scripts/build_cv.py | 191 +++++++++++++++++ scripts/extract_cv.py | 468 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 1045 insertions(+) create mode 100644 documents/JRM_CV.html create mode 100644 scripts/build_cv.py create mode 100644 scripts/extract_cv.py diff --git a/documents/JRM_CV.html b/documents/JRM_CV.html new file mode 100644 index 0000000..9303408 --- /dev/null +++ b/documents/JRM_CV.html @@ -0,0 +1,386 @@ + + + + + + Jeremy R. Manning, Ph.D. - Curriculum Vitae + + + +
+ Download CV as PDF +
+ +
+
+

Jeremy R. Manning, Ph.D.

+
+

Director, Contextual Dynamics Laboratory

+

Department of Psychological and Brain Sciences

+

Dartmouth College

+

HB 6207, Moore Hall

+

Hanover, NH 03755

+

U.S.A.

+

Email: jeremy.r.manning@dartmouth.edu

+

Phone: 603.646.2777

+

URL: +http://www.context-lab.com

+
+
+ +
+

Employment

+ Associate Professor, +Dartmouth College, Hanover, NH (2024 – )
+

Department of Psychological and Brain Sciences

+

Additional affiliation: Cognitive Science

+

Tenured: 2024

+Assistant Professor, +Dartmouth College, Hanover, NH (2015 – 2024)
+

Department of Psychological and Brain Sciences

+

Additional affiliation: Cognitive Science

+

Reappointed: 2018

+Postdoctoral Research + Associate, Princeton University, Princeton, NJ (2011 – 2015)
+

Princeton Neuroscience +Institute and Department of Computer Science

+

Advisors: Kenneth Norman, Ph.D. and David Blei, Ph.D.

+ +
+ +
+

Education

+ Ph.D. in Neuroscience, University of Pennsylvania, +Philadelphia, PA (2011)
+

Advisor: Michael Kahana, Ph.D.

+

Dissertation: Acquisition, storage, and + retrieval in digital and biological brains

+B.S. in Neuroscience (High honors, Magna cum laude), Brandeis University, +Waltham, MA (2006)
+

Advisor: Robert Sekuler, Ph.D.

+

Dissertation: Modeling human spatial navigation using a + degraded ideal navigator

+B.S. in Computer Science (Magna cum + laude), Brandeis University, +Waltham, MA (2006) + +
+ +
+

Grants, honors, and awards (selected)

+
    +
  1. Linda B. and Kendrick R. Wilson III 1969 Fellowship (2024–2025)
  2. +
  3. John M. Manley Huntington Award for Newly Tenured Faculty (2024)
    + + Awarded to recently tenured Dartmouth faculty members who have an outstanding record of teaching and research
  4. +
  5. CompX Faculty Grant (2024): Developing the next generation of multi-scale large language + models
    + + Award amount: $15,000; Role: PI
  6. +
  7. NSF CAREER Award (2022): Mapping and enhancing the acquisition +of conceptual knowledge using behavior, neural signals, and natural +language processing models
    + +Award amount: $881,612; Role: PI
  8. +
  9. Elected member, Memory Disorders Research Society (2021)
  10. +
  11. NIMH Grant (2021): Serotonin modulation of the development of +neural circuits underlying reward processing and impulsivity in +adolescents
    + +Award amount: $568,974; Role: Co-I (PI: Katherine Nautiyal)
  12. +
  13. NIH Grant Supplement (2019): Dissecting serotonergic and +dopaminergic contributions to the neural circuits underlying impulsive +behavior
    + +Award amount: $93,190; Role: Co-I (PI: Katherine Nautiyal)
  14. +
  15. National Institute on Drug Abuse Center for Technology and Behavioral Health Pilot Grant (2019): Linking +mental health and exercise via remote sensing
    + +Award amount: $20,000; Role: Co-PI (PI: David Bucci; Co-PI: Lorie +Loeb)
  16. +
  17. Dartmouth Junior Faculty Fellowship (2018)
  18. +
  19. Walter and Constance Burke Research Initiation Award (2018)
    + +Award amount: $25,000; Role: PI
  20. +
  21. DARPA Grant: Memory Enhancement with Modeling (MEM; 2018)
    + +Award amount: $55,558; Role: PI (sub-award of DARPA RAM +N66001-14-2-4-032)
  22. +
  23. i-CORPS Pilot Grant: Developing a mobile device for estimating +dynamic attention states (2018).
    +Award amount: $3,000; Role: Co-PI (PI: Peter Tse)
  24. +
  25. Diamond Research Development Award (2017): Improving memory and context reinstatement at perceptual event boundaries
    + +Award amount: $199,997; Role: Co-PI (PI: Barbara Jobst)
  26. +
  27. Dartmouth Leslie Center for the Humanities award for developing a +course incorporating the theme of "revolution" (2017; for +Storytelling with Data; PSYC 81.06).
    +Award amount: $5,000; +Role: Course Instructor
  28. +
  29. Social Impact Practicum (2017; for Storytelling with Data; PSYC + 81.06)
    +Award amount: $2,000; Role: Course Instructor
  30. +
  31. Young Minds and Brains (2017): The impact of exercise on attention, memory, +and stress
    + +Award amount: $100,000; Role: PI (with David Bucci, Co-PI)
  32. +
  33. NSF EPSCoR Grant (2016): The neural basis of attention
    +Award +amount: $6,000,000; Role: Co-I (PI: Peter Tse)
  34. +
  35. NIMH Ruth L. Kirshstein National Research Service Award for an + Individual Predoctoral Fellowship (2010): The neural representation of + context and its role in free recall
    +Award amount: $57,762; Role: + PI
  36. +
  37. NIH Computational Neuroscience Training Grant (2008)
    + +Role: Trainee
  38. +
  39. NIH Systems and Integrative Biology Training +Grant (2006)
    +Role: Trainee
  40. +
+ +
+ +
+

Publications

+ +
+

Book chapters

+

\end{etaremune}

+ +
+
+ +
+

Invited talks (selected)

+
    +
  1. Generative Episodic Memory: Constructing Scenarios of the Past (Keynote Speaker, 2025)
  2. +
  3. Brandeis University (2025)
  4. +
  5. University of Virginia (2024)
  6. +
  7. University of Pennsylvania (2023)
  8. +
  9. Cornell University (2023)
  10. +
  11. Boston University (2023)
  12. +
  13. Harvard University (2022)
  14. +
  15. University of California, Irvine (2022)
  16. +
  17. Ruhr Universität Bochum (2022)
  18. +
  19. Microsoft Research (2022)
  20. +
  21. Carnegie Mellon University (2021)
  22. +
  23. National Institutes of Mental Health (2021)
  24. +
  25. Boston College (2020)
  26. +
  27. Facebook Reality Labs (2020)
  28. +
  29. University of California, Berkeley (2020)
  30. +
  31. University of Oregon (2020)
  32. +
  33. Context and Episodic Memory Symposium (2019)
  34. +
  35. Society for Affective Science (2019)
  36. +
  37. Uber (2019)
  38. +
  39. Northeastern University (2018)
  40. +
  41. Society for Neuroscience (2018)
  42. +
  43. University of Pennsylvania (2018)
  44. +
  45. Bard College (2017)
  46. +
  47. Harvard University (2017)
  48. +
  49. University of Texas at Austin (2017)
  50. +
  51. Society for Neuroscience (2016)
  52. +
  53. Brown University (2015)
  54. +
  55. Columbia University (2015)
  56. +
  57. Dartmouth College (2015)
  58. +
  59. Georgetown University (2015)
  60. +
  61. Johns Hopkins University (2015)
  62. +
  63. Context and Episodic Memory Symposium (2014)
  64. +
  65. Manhattan Area Memory Meeting (2014)
  66. +
  67. Pattern Recognition in Neuroimaging (2014)
  68. +
  69. Context and Episodic Memory Symposium (2013)
  70. +
  71. University of Massachusetts, Amherst (2013)
  72. +
  73. Dartmouth College (2013)
  74. +
  75. Charles River Analytics (2012)
  76. +
  77. Natick Soldier Systems Center (2012)
  78. +
  79. Princeton University (2011)
  80. +
  81. Society for Mathematical Psychology (2011)
  82. +
  83. University of Pennsylvania (2011)
  84. +
+
+
+ +
+

Software (selected)

+
    +
  1. Manning JR, Manjunatha H, Kording K (2023) Chatify: add an LLM-based chatbot "tutor" to +Jupyter notebooks. GitHub.
  2. +
  3. Fitzpatrick PC, Manning JR (2022) Davos: import +Python packages, even if they aren't installed. GitHub.
  4. +
  5. Manning JR (2021) DataWrangler: format and clean +data, with a special focus on applying natural language processing +models to text data. GitHub.
  6. +
  7. Owen LLW, Chang TH, Manning JR (2019) Timecorr +Toolbox: compute high-order correlations in multivariate timeseries +data. GitHub.
  8. +
  9. Owen LLW, Heusser AC, Manning JR (2018) SuperEEG Toolbox: +infer full-brain activity patterns from a small(ish) number of ECoG electrodes using Gaussian process regression. GitHub.
  10. +
  11. Capota M, Turek J, Chen P-HC, Zhu X, Manning JR, Sundaram N, +Keller B, Wang Y, Shin YS (2017) BrainIAK: Brain Imaging Analysis Kit. brainiak.org.
  12. +
  13. Heusser AC, Ziman K, Fitzpatrick PC, Field CE, Manning JR +(2017) AutoFR: a scalable verbal free recall experiment with +automatic speech-to-text transcription. +GitHub.
  14. +
  15. Heusser AC, Fitzpatrick PC, Field CE, Ziman K, Manning JR +(2017) Quail: a Python toolbox for analyzing and plotting free recall +data. GitHub.
  16. +
  17. Heusser AC, Ziman K, Owen LLW, Manning JR (2017) +HyperTools: gain geometric insights into high-dimensional data (Python). GitHub.
  18. +
  19. Manning JR (2016) Hyperplot Tools: gain +geometric insights into high-dimensional data (MATLAB). + MATLAB + Central File Exchange: 56623.
  20. +
  21. Manning JR (2014) Hierarchical Topographic +Factor Analysis: + efficiently identify functional brain networks in fMRI data.
  22. +
  23. Manning JR (2013) MATLAB Ipsum: generate filler text using MATLAB. MATLAB + Central File Exchange: 43428.
  24. +
  25. Manning JR (2013) Easy resample: simple interface for + interpolating or resampling a 1d signal. MATLAB + Central File Exchange: 43320.
  26. +
  27. Manning JR (2012) Chuck Close-ify: automatically create + artwork in Chuck Close's iconic style based on existing photographs. MATLAB + Central File Exchange: 38770.
  28. +
  29. Manning JR (2012) Plot fMRI images: quick and easy method + for generating 2d and 3d brain plots. MATLAB Central File + Exchange: 36139.
  30. +
  31. Manning JR (2012) Generate synthetic fMRI data: generate + synthetic data for testing fMRI analyses and models. MATLAB Central File + Exchange: 36125.
  32. +
  33. Manning JR (2012) Sane pColor: create 2d images that + don't look blurry in OS X's Preview PDF viewer. MATLAB Central File + Exchange: 35601.
  34. +
  35. Manning JR (2012) Attach: MATLAB implementation of the attach + function in R. MATLAB Central File + Exchange: 35436.
  36. +
  37. Manning JR (2012) Get tight subplot handles: allows user to + exert finer control over subplot spacing in MATLAB. MATLAB Central File + Exchange: 35435.
  38. +
  39. Manning JR (2012) Slices: efficiently slice a tensor + along the nth dimension. MATLAB Central File + Exchange: 35439.
  40. +
+ +
+ +
+

Teaching and instruction

+
+

Open courses (selected)

+
    +
  1. Laboratory in Psychological Science; doi.org/10.5281/zenodo.6596761
  2. +
  3. Human + Memory; doi.org/10.5271/zenodo.5182787
  4. +
  5. Introduction to + Programming for Psychological Scientists; doi.org/10.5281/zenodo.5136795
  6. +
  7. Naturalistic + data analysis; doi.org/10.5281/zenodo.3937849
  8. +
  9. Storytelling + with Data; doi.org/10.5281/zenodo.5182774
  10. +
  11. Methods in Neuroscience at Dartmouth Computational + Summer School
  12. +
  13. Computational Neuroscience; doi.org/10.5281/zenodo.10235877
  14. +
+ +
+
+

Dartmouth College

+ +
+
+

Mentorship (selected)

+
    +
  1. Hung-Tu Chen (2024 – 2025; current position: Meta)
  2. +
  3. Gina Notaro (2017 – 2018; current position: HRL Laboratories)
  4. +
  5. Andrew Heusser (2016 – 2018; current position: PyMC Labs)
  6. +
+ +
+
+

Brandeis University

+ +
+
+ +
+

Service

+
+

Professional organizations

+
    +
  1. Dartmouth-Kalaniyot (2024 – ) Co-founder, Board member
  2. +
  3. National Science Foundation (2023, 2024, 2025) Panel member
  4. +
  5. NeuroMatch Academy (2021 – ) Developer and project mentor (computational neuroscience and deep learning tracks)
  6. +
  7. Artificial Intelligence and Statistics (AISTATS; 2021 – 2024) Area +chair (natural language processing and machine learning)
  8. +
  9. Methods in Neuroscience at Dartmouth (MIND) Summer School (2017 – ) Co-founder
  10. +
+ +
+
+

Dartmouth committee memberships

+
    +
  1. Social (Department Well-Being) Committee (2023–2024, 2024–2025)
  2. +
  3. Undergraduate Committee (2021–2022, 2015–2016)
  4. +
  5. Graduate Committee (2020–2021, 2016–2019)
  6. +
  7. Cognitive Neuroscience Faculty Search Committee (2018)
  8. +
  9. Molecular and Systems Biology Faculty Search Committee (2017)
  10. +
  11. Cognitive Neuroscience Faculty Search Committee (2016)
  12. +
+ +
+
+

Ad-hoc reviewer

+ Advances in Cognitive Psychology, +Agence Nationale de la Recherche, +American Journal of Psychology, +Cell Reports, +Cerebral Cortex, +Cognition, +Cognition and Emotion, +Cortex, +Computational and Systems Neuroscience (Cosyne), +eLife, +International Conference on Machine Learning (ICML), +International Joint Conference on Artificial Intelligence, +International Journal of Social Research Methodology, +Israel Science Foundation, +Journal of Cognitive Psychology, +Journal of Mathematical Psychology, +National Science Foundation (USA), +Nature, +Nature Communications, +Nature Computational Science, +Nature Human Behaviour, +Neural Computation, +NeuroImage, +Neural Information Processing Systems (NeurIPS), +Neuropsychologia, +PLoS Biology, +PLoS Computational Biology, +Proceedings of the National Academy of Sciences, +Psychological Reports, +Psychological Review, +Psychonomic Bulletin and Review, +Science, +Scientific Data, +Scientific Reports, +Society for Artificial Intelligence and Statistics (AISTATS), +Swiss National Science Foundation, +The Journal of Neuroscience +

\begin{center} +{\scriptsize Last updated: \today} +\end{center}

+ +
+
+ +
+

Last updated:

+ +
+
+ + diff --git a/scripts/build_cv.py b/scripts/build_cv.py new file mode 100644 index 0000000..c50ba0e --- /dev/null +++ b/scripts/build_cv.py @@ -0,0 +1,191 @@ +#!/usr/bin/env python3 +""" +Build CV from LaTeX source to PDF and HTML. + +This script: +1. Compiles JRM_CV.tex to PDF using XeLaTeX +2. Converts to HTML using custom LaTeX parser (extract_cv.py) +3. Cleans up temporary LaTeX build files +""" + +import subprocess +import sys +from pathlib import Path + +# Import the custom LaTeX parser +from extract_cv import extract_cv + +# Paths +PROJECT_ROOT = Path(__file__).parent.parent +DOCUMENTS_DIR = PROJECT_ROOT / 'documents' +DATA_DIR = PROJECT_ROOT / 'data' +CSS_DIR = PROJECT_ROOT / 'css' +TEX_FILE = DOCUMENTS_DIR / 'JRM_CV.tex' +PDF_FILE = DOCUMENTS_DIR / 'JRM_CV.pdf' +HTML_FILE = DOCUMENTS_DIR / 'JRM_CV.html' + +# LaTeX temporary file extensions to clean up +LATEX_TEMP_EXTENSIONS = [ + '.aux', '.log', '.out', '.toc', '.lof', '.lot', '.fls', '.fdb_latexmk', + '.synctex.gz', '.bbl', '.blg', '.nav', '.snm', '.vrb', + '.4ct', '.4tc', '.idv', '.lg', '.tmp', '.xdv', '.xref', '.dvi' +] + + +def run_command(cmd: list, cwd: Path = None, timeout: int = 120) -> tuple: + """Run a command and return (success, stdout, stderr).""" + try: + result = subprocess.run( + cmd, + cwd=cwd, + capture_output=True, + text=True, + timeout=timeout + ) + return result.returncode == 0, result.stdout, result.stderr + except subprocess.TimeoutExpired: + return False, '', f'Command timed out after {timeout}s' + except Exception as e: + return False, '', str(e) + + +def compile_pdf() -> bool: + """Compile LaTeX to PDF using XeLaTeX (run twice for references).""" + print(f"Compiling {TEX_FILE.name} to PDF...") + + # Run xelatex twice for references + for i in range(2): + success, stdout, stderr = run_command( + ['xelatex', '-interaction=nonstopmode', TEX_FILE.name], + cwd=DOCUMENTS_DIR, + timeout=120 + ) + if not success: + print(f"XeLaTeX pass {i+1} failed:") + print(stderr) + # Check if PDF was still created despite warnings + if not PDF_FILE.exists(): + return False + + if PDF_FILE.exists(): + size = PDF_FILE.stat().st_size + print(f"PDF generated: {PDF_FILE} ({size:,} bytes)") + return True + else: + print("PDF file not created") + return False + + +def compile_html() -> bool: + """Convert LaTeX to HTML using custom parser.""" + print(f"Converting {TEX_FILE.name} to HTML using custom parser...") + + success = extract_cv(TEX_FILE, HTML_FILE) + + if success and HTML_FILE.exists(): + size = HTML_FILE.stat().st_size + print(f"HTML generated: {HTML_FILE} ({size:,} bytes)") + return True + else: + print("HTML file not created") + return False + + +def cleanup_temp_files(): + """Remove temporary LaTeX build files.""" + print("Cleaning up temporary files...") + + cleaned = 0 + for ext in LATEX_TEMP_EXTENSIONS: + for f in DOCUMENTS_DIR.glob(f'*{ext}'): + try: + f.unlink() + cleaned += 1 + except Exception as e: + print(f"Could not remove {f}: {e}") + + print(f"Removed {cleaned} temporary files") + + +def validate_output() -> bool: + """Validate that PDF and HTML were generated correctly.""" + print("\nValidating output...") + + errors = [] + + # Check PDF + if not PDF_FILE.exists(): + errors.append("PDF file not found") + elif PDF_FILE.stat().st_size < 1000: + errors.append(f"PDF file too small ({PDF_FILE.stat().st_size} bytes)") + + # Check HTML + if not HTML_FILE.exists(): + errors.append("HTML file not found") + else: + with open(HTML_FILE, 'r', encoding='utf-8') as f: + html_content = f.read() + + # Check for key sections + required_sections = ['Employment', 'Education', 'Publications'] + for section in required_sections: + if section not in html_content: + errors.append(f"HTML missing section: {section}") + + # Check for download button + if 'cv-download-bar' not in html_content: + errors.append("HTML missing PDF download button") + + # Check for CSS link + if 'cv.css' not in html_content: + errors.append("HTML missing CSS link") + + if errors: + print("Validation errors:") + for error in errors: + print(f" - {error}") + return False + + print("Validation passed!") + print(f" PDF: {PDF_FILE} ({PDF_FILE.stat().st_size:,} bytes)") + print(f" HTML: {HTML_FILE} ({HTML_FILE.stat().st_size:,} bytes)") + return True + + +def build_cv() -> bool: + """Main build function.""" + print("=" * 60) + print("Building CV from LaTeX source") + print("=" * 60) + + # Check source file exists + if not TEX_FILE.exists(): + print(f"Error: Source file not found: {TEX_FILE}") + return False + + # Compile PDF + if not compile_pdf(): + print("Failed to compile PDF") + return False + + # Compile HTML using custom parser + if not compile_html(): + print("Failed to generate HTML") + return False + + # Clean up + cleanup_temp_files() + + # Validate + if not validate_output(): + return False + + print("\n" + "=" * 60) + print("CV build completed successfully!") + print("=" * 60) + return True + + +if __name__ == '__main__': + success = build_cv() + sys.exit(0 if success else 1) diff --git a/scripts/extract_cv.py b/scripts/extract_cv.py new file mode 100644 index 0000000..d11a667 --- /dev/null +++ b/scripts/extract_cv.py @@ -0,0 +1,468 @@ +#!/usr/bin/env python3 +""" +Custom LaTeX to HTML converter for JRM_CV.tex. + +This parser handles the specific LaTeX constructs used in the CV +and produces HTML that matches the PDF formatting exactly. +""" + +import re +from pathlib import Path +from typing import Dict, List, Optional +from dataclasses import dataclass, field + + +@dataclass +class CVSection: + """Represents a section of the CV.""" + title: str + content: str + subsections: List['CVSection'] = field(default_factory=list) + + +def read_latex_file(filepath: Path) -> str: + """Read and return the content of a LaTeX file.""" + with open(filepath, 'r', encoding='utf-8') as f: + return f.read() + + +def extract_document_body(latex: str) -> str: + """Extract content between begin document and end document.""" + match = re.search(r'\\begin\{document\}(.+?)\\end\{document\}', latex, re.DOTALL) + if match: + return match.group(1) + return latex + + +def balanced_braces_extract(text: str, start: int) -> tuple: + """Extract content within balanced braces starting at position start. + Returns (content, end_position) or (None, -1) if not found.""" + if start >= len(text) or text[start] != '{': + return None, -1 + + depth = 0 + content_start = start + 1 + + for i in range(start, len(text)): + if text[i] == '{': + depth += 1 + elif text[i] == '}': + depth -= 1 + if depth == 0: + return text[content_start:i], i + 1 + + return None, -1 + + +def convert_command(text: str, cmd: str, html_start: str, html_end: str) -> str: + """Convert a LaTeX command to HTML tags.""" + pattern = '\\' + cmd + '{' + result = [] + i = 0 + + while i < len(text): + pos = text.find(pattern, i) + if pos == -1: + result.append(text[i:]) + break + + result.append(text[i:pos]) + content, end_pos = balanced_braces_extract(text, pos + len(pattern) - 1) + + if content is not None: + result.append(html_start) + result.append(content) + result.append(html_end) + i = end_pos + else: + result.append(text[pos:pos + len(pattern)]) + i = pos + len(pattern) + + return ''.join(result) + + +def convert_href(text: str) -> str: + """Convert href commands to HTML links.""" + result = [] + i = 0 + + while i < len(text): + match = re.search(r'\\href\{', text[i:]) + if not match: + result.append(text[i:]) + break + + pos = i + match.start() + result.append(text[i:pos]) + + # \href{ is 6 chars, { is at position 5 from match start + brace_pos = pos + 5 + + # Extract URL + url, url_end = balanced_braces_extract(text, brace_pos) + if url is None: + result.append(text[pos:pos + 6]) + i = pos + 6 + continue + + # Extract link text + link_text, text_end = balanced_braces_extract(text, url_end) + if link_text is None: + result.append(text[pos:url_end]) + i = url_end + continue + + result.append(f'{link_text}') + i = text_end + + return ''.join(result) + + +def convert_latex_formatting(text: str) -> str: + """Convert LaTeX formatting commands to HTML.""" + # Remove LaTeX comments (lines starting with %) + text = re.sub(r'^%.*$', '', text, flags=re.MULTILINE) + text = re.sub(r'(?', '') + text = convert_command(text, 'textit', '', '') + text = convert_command(text, 'emph', '', '') + text = convert_command(text, 'textsc', '', '') + text = convert_command(text, 'ul', '', '') + text = convert_command(text, 'texttt', '', '') + text = convert_command(text, 'textsuperscript', '', '') + + # Handle {\bf text} style (old LaTeX) + text = re.sub(r'\{\\bf\s+([^}]+)\}', r'\1', text) + text = re.sub(r'\{\\it\s+([^}]+)\}', r'\1', text) + text = re.sub(r'\{\\sc\s+([^}]+)\}', r'\1', text) + + # Handle special characters + replacements = [ + (r'\&', '&'), + (r'\_', '_'), + (r'\%', '%'), + (r'\$', '$'), + (r'\#', '#'), + (r'\-', ''), # discretionary hyphen + ('``', '"'), + ("''", '"'), + ('`', "'"), + ("'", "'"), + ('---', '—'), # em-dash (check before en-dash) + ('--', '–'), # en-dash + ('~', ' '), # non-breaking space + (r'\,', ' '), # thin space + (r'\"a', 'ä'), + (r'\"o', 'ö'), + (r'\"u', 'ü'), + (r'\"{a}', 'ä'), + (r'\"{o}', 'ö'), + (r'\"{u}', 'ü'), + ] + + for old, new in replacements: + text = text.replace(old, new) + + # Line breaks with spacing + text = re.sub(r'\\\\\[[\d.]+cm\]', '
\n', text) + text = text.replace(r'\\', '
\n') + + # Remove commands we don't need + text = re.sub(r'\\blfootnote\{[^}]*\}', '', text) + text = re.sub(r'\\vspace\{[^}]*\}', '', text) + text = re.sub(r'\\hspace\{[^}]*\}', '', text) + text = re.sub(r'\\noindent\s*', '', text) + + # Math mode: $...$ - simple handling + text = re.sub(r'\$([^$]+)\$', r'\1', text) + + # Superscripts in math + text = re.sub(r'\^\\mathrm\{([^}]+)\}', r'\1', text) + text = re.sub(r'\^\{([^}]+)\}', r'\1', text) + + return text + + +def parse_etaremune(content: str) -> List[str]: + """Parse etaremune environment (reverse-numbered list) and return items.""" + items = [] + + # Find etaremune content + match = re.search(r'\\begin\{etaremune\}(.+?)\\end\{etaremune\}', content, re.DOTALL) + if not match: + return items + + list_content = match.group(1) + + # Split by item + parts = re.split(r'\\item\s*', list_content) + + for part in parts: + part = part.strip() + if part: + items.append(convert_latex_formatting(part)) + + return items + + +def parse_multicol_etaremune(content: str) -> List[str]: + """Parse multicol environment containing etaremune.""" + # Remove multicol wrapper + content = re.sub(r'\\begin\{multicols\}\{\d+\}', '', content) + content = re.sub(r'\\end\{multicols\}', '', content) + + return parse_etaremune(content) + + +def extract_header_info(body: str) -> Dict[str, str]: + """Extract header information (name, title, contact).""" + info = {} + + # Find header section (before first section) + header_match = re.search(r'^(.+?)\\section\*', body, re.DOTALL) + if header_match: + header = header_match.group(1) + + # Name - find the LARGE block and extract everything until the line break + name_match = re.search(r'\{\\LARGE\s*(.+?)\}\\\\', header, re.DOTALL) + if name_match: + name_raw = name_match.group(1).strip() + info['name'] = convert_latex_formatting(name_raw) + + # Find the position after the LARGE block (handles nested braces) + large_match = re.search(r'\{\\LARGE', header) + if large_match: + # Find the matching closing brace + start_pos = large_match.start() + _, end_pos = balanced_braces_extract(header, start_pos) + if end_pos > 0: + # Skip past the \\ after the closing brace + if header[end_pos:end_pos+2] == '\\\\': + end_pos += 2 + # Skip any spacing like [0.25cm] + spacing_match = re.match(r'\[[\d.]+cm\]', header[end_pos:]) + if spacing_match: + end_pos += spacing_match.end() + rest_of_header = header[end_pos:] + else: + rest_of_header = header + else: + rest_of_header = header + + # Split by line breaks + parts = re.split(r'\\\\(?:\[[\d.]+cm\])?', rest_of_header) + + lines = [] + for part in parts: + # Remove LaTeX comments + part = re.sub(r'%.*$', '', part, flags=re.MULTILINE) + part = part.strip() + + # Skip empty parts and stray braces + if part and part not in ['}', '{', '']: + converted = convert_latex_formatting(part) + converted = converted.strip() + # Skip empty results or just punctuation + if converted and converted not in ['}', '{', '']: + lines.append(converted) + + info['header_lines'] = lines + + return info + + +def extract_sections(body: str) -> List[CVSection]: + """Extract all sections from the CV.""" + sections = [] + + # Split by section* or section + section_pattern = r'\\section\*?\{([^}]+)\}' + parts = re.split(section_pattern, body) + + # parts[0] is header, parts[1] is first section title, parts[2] is content, etc. + if len(parts) > 1: + for i in range(1, len(parts), 2): + if i + 1 < len(parts): + title = parts[i].strip() + content = parts[i + 1].strip() + + # Check for subsections + subsection_pattern = r'\\subsection\*?\{([^}]+)\}' + sub_parts = re.split(subsection_pattern, content) + + if len(sub_parts) > 1: + section = CVSection(title=title, content=sub_parts[0].strip()) + for j in range(1, len(sub_parts), 2): + if j + 1 < len(sub_parts): + sub_title = sub_parts[j].strip() + sub_content = sub_parts[j + 1].strip() + section.subsections.append(CVSection(title=sub_title, content=sub_content)) + sections.append(section) + else: + sections.append(CVSection(title=title, content=content)) + + return sections + + +def render_list_items(items: List[str], reversed_numbering: bool = True) -> str: + """Render a list of items as HTML ordered list.""" + if not items: + return '' + + if reversed_numbering: + html = f'
    \n' + else: + html = '
      \n' + + for item in items: + item = item.strip() + # Clean up leading/trailing breaks + item = re.sub(r'^
      \s*', '', item) + item = re.sub(r'\s*
      \s*$', '', item) + html += f'
    1. {item}
    2. \n' + + html += '
    \n' + return html + + +def render_section_content(content: str, section_title: str) -> str: + """Render section content to HTML based on section type.""" + + # Check for etaremune lists + if r'\begin{etaremune}' in content: + if r'\begin{multicols}' in content: + items = parse_multicol_etaremune(content) + if 'talks' in section_title.lower() or 'undergraduate' in section_title.lower(): + return f'
    {render_list_items(items)}
    ' + else: + return render_list_items(items) + else: + items = parse_etaremune(content) + return render_list_items(items) + + # For regular content, convert formatting + content = convert_latex_formatting(content) + + # Split into paragraphs + paragraphs = re.split(r'\n\s*\n', content) + + html = '' + for para in paragraphs: + para = para.strip() + if para: + if not para.startswith('<'): + html += f'

    {para}

    \n' + else: + html += f'{para}\n' + + return html + + +def generate_html(tex_content: str) -> str: + """Generate complete HTML from LaTeX content.""" + body = extract_document_body(tex_content) + header_info = extract_header_info(body) + sections = extract_sections(body) + + html_parts = [] + + # HTML header + html_parts.append(''' + + + + + Jeremy R. Manning, Ph.D. - Curriculum Vitae + + + + + +
    +''') + + # Header section + html_parts.append('
    \n') + if 'name' in header_info: + html_parts.append(f'

    {header_info["name"]}

    \n') + if 'header_lines' in header_info: + html_parts.append('
    \n') + for line in header_info['header_lines']: + if line.strip(): + html_parts.append(f'

    {line}

    \n') + html_parts.append('
    \n') + html_parts.append('
    \n\n') + + # Sections + for section in sections: + section_id = section.title.lower() + section_id = re.sub(r'[^a-z0-9]+', '-', section_id).strip('-') + html_parts.append(f'
    \n') + html_parts.append(f'

    {section.title}

    \n') + + if section.subsections: + if section.content.strip(): + rendered = render_section_content(section.content, section.title) + html_parts.append(f' {rendered}\n') + + for subsection in section.subsections: + sub_id = re.sub(r'[^a-z0-9]+', '-', subsection.title.lower()).strip('-') + html_parts.append(f'
    \n') + html_parts.append(f'

    {subsection.title}

    \n') + rendered = render_section_content(subsection.content, subsection.title) + html_parts.append(f' {rendered}\n') + html_parts.append('
    \n') + else: + rendered = render_section_content(section.content, section.title) + html_parts.append(f' {rendered}\n') + + html_parts.append('
    \n\n') + + # Footer + html_parts.append('''
    +

    Last updated:

    + +
    +
    + + +''') + + return ''.join(html_parts) + + +def extract_cv(input_path: Path, output_path: Path) -> bool: + """Main extraction function.""" + try: + tex_content = read_latex_file(input_path) + html_content = generate_html(tex_content) + + with open(output_path, 'w', encoding='utf-8') as f: + f.write(html_content) + + return True + except Exception as e: + print(f"Error extracting CV: {e}") + import traceback + traceback.print_exc() + return False + + +if __name__ == '__main__': + project_root = Path(__file__).parent.parent + input_file = project_root / 'documents' / 'JRM_CV.tex' + output_file = project_root / 'documents' / 'JRM_CV.html' + + if extract_cv(input_file, output_file): + print(f"Successfully generated {output_file}") + else: + print("Failed to generate HTML") From 9506acd0b36253102fcad1bcdce32bfc36be967f Mon Sep 17 00:00:00 2001 From: Jeremy Manning Date: Sun, 14 Dec 2025 06:16:13 -0500 Subject: [PATCH 2/4] Add CV auto-build infrastructure: workflow, CSS, tests, and HTML link update MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add GitHub Actions workflow (.github/workflows/build-cv.yml) to auto-build CV PDF and HTML from LaTeX source on push to main - Add CSS stylesheet (css/cv.css) with @font-face for Dartmouth Ruzicka font, responsive design, and print styles - Add comprehensive test suite (tests/test_build_cv.py) with 61 real tests covering LaTeX conversion, HTML generation, and PDF compilation - Update people.html CV link to point to HTML version instead of PDF - Regenerate CV PDF with latest content 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .github/workflows/build-cv.yml | 76 +++ css/cv.css | 608 +++++++++++++++++++++++ data/people.xlsx | Bin 13074 -> 12925 bytes documents/JRM_CV.pdf | Bin 104045 -> 105539 bytes people.html | 2 +- tests/test_build_cv.py | 854 +++++++++++++++++++++++++++++++++ 6 files changed, 1539 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/build-cv.yml create mode 100644 css/cv.css create mode 100644 tests/test_build_cv.py diff --git a/.github/workflows/build-cv.yml b/.github/workflows/build-cv.yml new file mode 100644 index 0000000..bb01164 --- /dev/null +++ b/.github/workflows/build-cv.yml @@ -0,0 +1,76 @@ +name: Build CV + +on: + push: + branches: + - main + paths: + - 'documents/JRM_CV.tex' + - 'scripts/build_cv.py' + - 'scripts/extract_cv.py' + - 'css/cv.css' + - '.github/workflows/build-cv.yml' + pull_request: + branches: + - main + paths: + - 'documents/JRM_CV.tex' + - 'scripts/build_cv.py' + - 'scripts/extract_cv.py' + - 'css/cv.css' + - '.github/workflows/build-cv.yml' + workflow_dispatch: # Allow manual triggering + +jobs: + build-cv: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install TeX Live + run: | + sudo apt-get update + sudo apt-get install -y texlive-xetex texlive-fonts-extra texlive-latex-extra + + - name: Install Dartmouth Ruzicka font + run: | + mkdir -p ~/.fonts + cp data/DartmouthRuzicka-*.ttf ~/.fonts/ + fc-cache -fv + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + cache: 'pip' + cache-dependency-path: 'requirements-build.txt' + + - name: Install Python dependencies + run: pip install -r requirements-build.txt + + - name: Build CV (PDF and HTML) + working-directory: scripts + run: python build_cv.py + + - name: Run CV tests + run: python -m pytest tests/test_build_cv.py -v + + - name: Check for changes + id: check_changes + run: | + if [[ -n $(git status --porcelain documents/JRM_CV.pdf documents/JRM_CV.html) ]]; then + echo "changes=true" >> $GITHUB_OUTPUT + else + echo "changes=false" >> $GITHUB_OUTPUT + fi + + - name: Commit and push changes + if: github.event_name == 'push' && steps.check_changes.outputs.changes == 'true' + run: | + git config user.name 'github-actions[bot]' + git config user.email 'github-actions[bot]@users.noreply.github.com' + git add documents/JRM_CV.pdf documents/JRM_CV.html + git diff --staged --quiet || git commit -m "Auto-build: Update CV PDF and HTML from LaTeX source" + git push diff --git a/css/cv.css b/css/cv.css new file mode 100644 index 0000000..fbe078e --- /dev/null +++ b/css/cv.css @@ -0,0 +1,608 @@ +/* ========================================================================== + CV Stylesheet + ========================================================================== */ + +/* Font Face Declarations + ========================================================================== */ + +@font-face { + font-family: 'Dartmouth Ruzicka'; + src: url('../data/DartmouthRuzicka-Regular.ttf') format('truetype'); + font-weight: normal; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: 'Dartmouth Ruzicka'; + src: url('../data/DartmouthRuzicka-Bold.ttf') format('truetype'); + font-weight: bold; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: 'Dartmouth Ruzicka'; + src: url('../data/DartmouthRuzicka-RegularItalic.ttf') format('truetype'); + font-weight: normal; + font-style: italic; + font-display: swap; +} + +@font-face { + font-family: 'Dartmouth Ruzicka'; + src: url('../data/DartmouthRuzicka-BoldItalic.ttf') format('truetype'); + font-weight: bold; + font-style: italic; + font-display: swap; +} + +/* CSS Variables + ========================================================================== */ + +:root { + --primary-green: rgb(0, 112, 60); + --bg-green: rgba(0, 112, 60, 0.2); + --dark-text: rgba(0, 0, 0, 0.7); + --light-gray: #f5f5f5; + --border-gray: #e0e0e0; + --max-width: 900px; + --spacing-unit: 1rem; +} + +/* Base Styles + ========================================================================== */ + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Dartmouth Ruzicka', Georgia, serif; + font-size: 11pt; + line-height: 1.6; + color: var(--dark-text); + background-color: white; + padding-top: 60px; /* Space for sticky download bar */ +} + +/* Download Bar + ========================================================================== */ + +.cv-download-bar { + position: fixed; + top: 0; + left: 0; + right: 0; + background-color: var(--primary-green); + color: white; + padding: 0.75rem 2rem; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); + z-index: 1000; + display: flex; + justify-content: space-between; + align-items: center; +} + +.cv-download-bar .bar-content { + max-width: var(--max-width); + width: 100%; + margin: 0 auto; + display: flex; + justify-content: space-between; + align-items: center; +} + +.cv-download-bar .cv-title { + font-size: 1.1rem; + font-weight: bold; +} + +.cv-download-bar .download-btn { + background-color: white; + color: var(--primary-green); + border: none; + padding: 0.5rem 1.5rem; + font-family: 'Dartmouth Ruzicka', Georgia, serif; + font-size: 0.95rem; + font-weight: bold; + border-radius: 4px; + cursor: pointer; + text-decoration: none; + display: inline-block; + transition: all 0.3s ease; +} + +.cv-download-bar .download-btn:hover { + background-color: var(--bg-green); + color: white; + transform: translateY(-2px); + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); +} + +/* Main Content Container + ========================================================================== */ + +.cv-content { + max-width: var(--max-width); + margin: 2rem auto; + padding: 2rem; + background-color: white; + box-shadow: 0 0 20px rgba(0, 0, 0, 0.1); +} + +/* CV Header + ========================================================================== */ + +.cv-header { + text-align: center; + margin-bottom: 3rem; + padding-bottom: 2rem; + border-bottom: 2px solid var(--primary-green); +} + +.cv-header h1 { + font-size: 2.5rem; + font-weight: bold; + color: var(--primary-green); + margin-bottom: 1rem; + letter-spacing: 0.5px; +} + +.cv-header .contact-info { + font-size: 0.95rem; + line-height: 1.8; + color: var(--dark-text); +} + +.cv-header .contact-info p { + margin: 0.25rem 0; +} + +/* Headings + ========================================================================== */ + +h2 { + font-size: 1.4rem; + font-weight: bold; + color: var(--primary-green); + margin-top: 2rem; + margin-bottom: 1rem; + padding-bottom: 0.5rem; + border-bottom: 1px solid var(--border-gray); + text-transform: uppercase; + letter-spacing: 1px; +} + +h3 { + font-size: 1.1rem; + font-weight: bold; + color: var(--dark-text); + margin-top: 1.5rem; + margin-bottom: 0.75rem; +} + +h4 { + font-size: 1rem; + font-weight: bold; + color: var(--dark-text); + margin-top: 1rem; + margin-bottom: 0.5rem; +} + +/* Lists + ========================================================================== */ + +ul, ol { + margin-left: 2rem; + margin-bottom: 1rem; +} + +li { + margin-bottom: 0.75rem; + line-height: 1.6; +} + +/* Reverse-numbered lists for publications and awards */ +ol[reversed] { + list-style: none; + counter-reset: item; +} + +ol[reversed] > li { + counter-increment: item -1; + position: relative; + padding-left: 2.5rem; +} + +ol[reversed] > li::before { + content: counter(item) "."; + position: absolute; + left: 0; + font-weight: bold; + color: var(--primary-green); + min-width: 2rem; + text-align: right; + padding-right: 0.5rem; +} + +/* Paragraphs + ========================================================================== */ + +p { + margin-bottom: 1rem; + text-align: justify; +} + +/* Links + ========================================================================== */ + +a { + color: var(--primary-green); + text-decoration: none; + transition: all 0.2s ease; +} + +a:hover { + color: var(--dark-text); + text-decoration: underline; +} + +/* Text Styles + ========================================================================== */ + +.small-caps { + font-variant: small-caps; + letter-spacing: 0.5px; +} + +.underline { + text-decoration: underline; +} + +em, i { + font-style: italic; +} + +strong, b { + font-weight: bold; +} + +/* Two-Column Lists + ========================================================================== */ + +.two-column-list { + column-count: 2; + column-gap: 2rem; + margin-bottom: 1.5rem; +} + +.two-column-list li { + break-inside: avoid; + page-break-inside: avoid; +} + +/* Special Sections + ========================================================================== */ + +.employment-entry, +.education-entry, +.award-entry { + margin-bottom: 1.5rem; +} + +.employment-entry .position { + font-weight: bold; + color: var(--dark-text); +} + +.employment-entry .institution { + font-style: italic; + margin-left: 0.5rem; +} + +.employment-entry .dates { + color: var(--primary-green); + font-weight: bold; +} + +.education-entry .degree { + font-weight: bold; +} + +.education-entry .institution { + font-style: italic; +} + +/* Publications + ========================================================================== */ + +.publication { + margin-bottom: 1rem; + text-align: justify; +} + +.publication .authors { + font-weight: normal; +} + +.publication .title { + font-weight: bold; +} + +.publication .journal { + font-style: italic; +} + +.publication .year { + color: var(--primary-green); +} + +/* CV Footer + ========================================================================== */ + +.cv-footer { + margin-top: 4rem; + padding-top: 2rem; + border-top: 2px solid var(--primary-green); + text-align: center; + font-size: 0.9rem; + color: var(--dark-text); + font-style: italic; +} + +/* Responsive Design - Tablet + ========================================================================== */ + +@media screen and (max-width: 768px) { + body { + padding-top: 80px; + } + + .cv-download-bar { + padding: 0.75rem 1rem; + } + + .cv-download-bar .bar-content { + flex-direction: column; + gap: 0.5rem; + } + + .cv-download-bar .cv-title { + font-size: 1rem; + } + + .cv-content { + margin: 1rem; + padding: 1.5rem; + } + + .cv-header h1 { + font-size: 2rem; + } + + h2 { + font-size: 1.2rem; + } + + h3 { + font-size: 1rem; + } + + .two-column-list { + column-count: 1; + } + + ul, ol { + margin-left: 1.5rem; + } + + ol[reversed] > li { + padding-left: 2rem; + } +} + +/* Responsive Design - Mobile + ========================================================================== */ + +@media screen and (max-width: 480px) { + body { + font-size: 10pt; + padding-top: 90px; + } + + .cv-download-bar { + padding: 0.5rem; + } + + .cv-content { + margin: 0.5rem; + padding: 1rem; + box-shadow: none; + } + + .cv-header { + margin-bottom: 2rem; + padding-bottom: 1.5rem; + } + + .cv-header h1 { + font-size: 1.75rem; + } + + .cv-header .contact-info { + font-size: 0.85rem; + } + + h2 { + font-size: 1.1rem; + margin-top: 1.5rem; + } + + h3 { + font-size: 0.95rem; + } + + ul, ol { + margin-left: 1rem; + } + + ol[reversed] > li { + padding-left: 1.5rem; + } + + ol[reversed] > li::before { + min-width: 1.5rem; + } + + .cv-footer { + margin-top: 3rem; + padding-top: 1.5rem; + font-size: 0.85rem; + } +} + +/* Print Styles + ========================================================================== */ + +@media print { + @page { + margin: 0.75in; + size: letter; + } + + body { + font-size: 10pt; + padding-top: 0; + background: white; + } + + .cv-download-bar { + display: none; + } + + .cv-content { + max-width: 100%; + margin: 0; + padding: 0; + box-shadow: none; + } + + .cv-header { + page-break-after: avoid; + border-bottom: 2px solid var(--primary-green); + } + + .cv-header h1 { + font-size: 20pt; + color: var(--primary-green); + } + + h2 { + font-size: 13pt; + page-break-after: avoid; + border-bottom: 1px solid var(--border-gray); + color: var(--primary-green); + } + + h3 { + font-size: 11pt; + page-break-after: avoid; + } + + h4 { + font-size: 10pt; + page-break-after: avoid; + } + + .employment-entry, + .education-entry, + .award-entry, + .publication { + page-break-inside: avoid; + } + + li { + page-break-inside: avoid; + } + + .two-column-list { + column-count: 2; + column-gap: 1.5rem; + } + + a { + color: var(--primary-green); + text-decoration: none; + } + + a[href^="http"]:after { + content: ""; + } + + .cv-footer { + margin-top: 2rem; + padding-top: 1rem; + border-top: 1px solid var(--border-gray); + page-break-before: avoid; + } + + /* Ensure proper page breaks */ + section { + page-break-inside: avoid; + } + + /* Orphan and widow control */ + p, li { + orphans: 3; + widows: 3; + } + + /* Color adjustments for printing */ + * { + color-adjust: exact; + -webkit-print-color-adjust: exact; + print-color-adjust: exact; + } +} + +/* Print optimization for publication lists */ +@media print { + ol[reversed] > li::before { + color: black; + } +} + +/* Utility Classes + ========================================================================== */ + +.no-break { + page-break-inside: avoid; + break-inside: avoid; +} + +.text-center { + text-align: center; +} + +.text-right { + text-align: right; +} + +.text-justify { + text-align: justify; +} + +.mb-0 { margin-bottom: 0; } +.mb-1 { margin-bottom: 0.5rem; } +.mb-2 { margin-bottom: 1rem; } +.mb-3 { margin-bottom: 1.5rem; } +.mb-4 { margin-bottom: 2rem; } + +.mt-0 { margin-top: 0; } +.mt-1 { margin-top: 0.5rem; } +.mt-2 { margin-top: 1rem; } +.mt-3 { margin-top: 1.5rem; } +.mt-4 { margin-top: 2rem; } diff --git a/data/people.xlsx b/data/people.xlsx index 2402916f5162b6eca2f2014d38f75227c5e4455f..49d2827f44d679b2be4d2854fa55036f6ddca9f9 100644 GIT binary patch delta 5850 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 cUl>YwEb%7 delta 5932 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 diff --git a/documents/JRM_CV.pdf b/documents/JRM_CV.pdf index 2265ac63a2291af00a6652af6ddac90fe0079508..3f343cb4aca85a7be562a2d6c2a5dded2287d358 100644 GIT binary patch delta 40487 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 z z%$UTFUSd#lAkhovb;*I4t1Np6D(_K6!qSotV%Wzf_4!K??8BC?fQ0J2^j&_w@T=a* zg@uYg)HDK1My+qQO9*NV-LsbR)7W157+Laq^k2qcKzP&QN{09;-rznAkSX;V!FEdX zgXIK|LxXyZ{BP^O2wWQupiVqtJuRDJ{4b8?{9n5?EjR$fOqibm#PDB(Dx{?N^n6`y z`HY~pc6_Rercxp{Zw2_Rt*ymCwt_Y`LSk>k#e_t~LXfQ$FTJV z>lo{CsOtdpQ_$AAXb5l|SmdToFpxxaNb&@qi}|0X>rY$TXCqq~LvIB#h>3^n+!b-( z;@aGx%Wlv_9hm5aG`x8c(699^;+$)vC8hO757BqIqDCjv%Mze-iu}6pJX7P;zlNsw z6<2{R5aR|~!tT-+T6Zdz+mqyjtF=k#VZ%sXfHl5u+!45Y%0zC1V@s=*#)=hxw>jA1t)_kguRm&hv4!KJ@ff zr0pjnB7aaI8}2ket!a7QpzFO~d1TSzvhL$d-3E>hDm0|mJp;+xo{ zt)+vSxG4R0%K5C!gd4Yz`GfR_mikGD5K+~~FO9{SckFN!3|*Q3ha|PThc(>82X1SJ S%MTI~7vjfdWmVEr#{EB5O46nP delta 38971 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 bo^{}g%Vg8M2QUx{2nvG<*x2MWWe're a motley crew of science nerds united in the pursuit of understanding

    jeremy manning | lab director

    Jeremy is an Associate Professor of Psychological and Brain Sciences at Dartmouth and directs the Contextual Dynamics Lab. He enjoys thinking about brains, non-brain brain-related things (e.g. zombies), computers, annoying-to-solve puzzles, and cats.

    -

    [CV] [Google Scholar]

    +

    [CV] [Google Scholar]

    diff --git a/tests/test_build_cv.py b/tests/test_build_cv.py new file mode 100644 index 0000000..6657ac5 --- /dev/null +++ b/tests/test_build_cv.py @@ -0,0 +1,854 @@ +"""Tests for CV build system using REAL files - no mocks. + +All tests use real file operations, real PDF compilation, and real HTML generation +to verify the actual behavior of the CV build scripts. + +IMPORTANT: NO MOCKS OR SIMULATIONS - all tests use real files and real operations. +""" +import pytest +from pathlib import Path +import tempfile +import subprocess +import re + +from extract_cv import ( + read_latex_file, + extract_document_body, + balanced_braces_extract, + convert_command, + convert_href, + convert_latex_formatting, + parse_etaremune, + extract_header_info, + extract_sections, + generate_html, + extract_cv, +) + +from build_cv import ( + run_command, + compile_pdf, + compile_html, + cleanup_temp_files, + validate_output, + build_cv, + PDF_FILE, + TEX_FILE, + HTML_FILE, +) + + +class TestLatexConversion: + """Test LaTeX conversion functions.""" + + def test_convert_textbf(self): + """Test that \\textbf is converted to .""" + text = r'\textbf{bold text}' + result = convert_latex_formatting(text) + assert 'bold text' in result + + def test_convert_textbf_nested(self): + """Test nested braces in \\textbf.""" + text = r'\textbf{text with {nested} braces}' + result = convert_latex_formatting(text) + assert 'text with {nested} braces' in result + + def test_convert_href_basic(self): + """Test basic \\href conversion.""" + text = r'\href{https://example.com}{Link Text}' + result = convert_latex_formatting(text) + assert 'Link Text' in result + + def test_convert_href_in_sentence(self): + """Test \\href within a sentence.""" + text = r'Visit \href{https://example.com}{our site} for more.' + result = convert_latex_formatting(text) + assert 'Visit our site for more.' in result + + def test_convert_multiple_hrefs(self): + """Test multiple \\href commands.""" + text = r'\href{http://a.com}{Link A} and \href{http://b.com}{Link B}' + result = convert_latex_formatting(text) + assert 'Link A' in result + assert 'Link B' in result + + def test_convert_textit(self): + """Test \\textit conversion.""" + text = r'\textit{italic text}' + result = convert_latex_formatting(text) + assert 'italic text' in result + + def test_convert_emph(self): + """Test \\emph conversion.""" + text = r'\emph{emphasized}' + result = convert_latex_formatting(text) + assert 'emphasized' in result + + def test_convert_textsc(self): + """Test \\textsc conversion.""" + text = r'\textsc{Small Caps}' + result = convert_latex_formatting(text) + assert 'Small Caps' in result + + def test_convert_special_ampersand(self): + """Test \\& conversion.""" + text = r'Smith \& Jones' + result = convert_latex_formatting(text) + assert 'Smith & Jones' in result + + def test_convert_special_underscore(self): + """Test \\_ conversion.""" + text = r'file\_name' + result = convert_latex_formatting(text) + assert 'file_name' in result + + def test_convert_special_percent(self): + """Test \\% conversion.""" + text = r'50\% off' + result = convert_latex_formatting(text) + assert '50% off' in result + + def test_convert_special_dollar(self): + """Test \\$ conversion.""" + text = r'\$100' + result = convert_latex_formatting(text) + assert '$100' in result + + def test_convert_em_dash(self): + """Test --- to em-dash conversion.""" + text = 'Hello---world' + result = convert_latex_formatting(text) + assert 'Hello—world' in result + + def test_convert_en_dash(self): + """Test -- to en-dash conversion.""" + text = '2020--2025' + result = convert_latex_formatting(text) + assert '2020–2025' in result + + def test_convert_quotes(self): + """Test quote conversion.""" + text = "``Hello'' and `world'" + result = convert_latex_formatting(text) + assert '"Hello"' in result + assert "'world'" in result + + def test_convert_linebreak(self): + """Test \\\\ to
    conversion.""" + text = r'Line 1\\Line 2' + result = convert_latex_formatting(text) + assert '
    ' in result + + def test_convert_combined_formatting(self): + """Test multiple formatting commands together.""" + text = r'\textbf{Bold} and \textit{italic} and \href{http://example.com}{link}' + result = convert_latex_formatting(text) + assert 'Bold' in result + assert 'italic' in result + assert 'link' in result + + def test_balanced_braces_extract(self): + """Test balanced braces extraction.""" + text = '{simple content}' + content, end = balanced_braces_extract(text, 0) + assert content == 'simple content' + assert end == len(text) + + def test_balanced_braces_nested(self): + """Test nested braces extraction.""" + text = '{outer {inner} text}' + content, end = balanced_braces_extract(text, 0) + assert content == 'outer {inner} text' + + def test_balanced_braces_multiple_levels(self): + """Test deeply nested braces.""" + text = '{a {b {c} d} e}' + content, end = balanced_braces_extract(text, 0) + assert content == 'a {b {c} d} e' + + def test_convert_command_basic(self): + """Test convert_command function.""" + text = r'\textbf{bold}' + result = convert_command(text, 'textbf', '', '') + assert result == 'bold' + + def test_convert_href_function(self): + """Test convert_href function.""" + text = r'\href{http://example.com}{Link}' + result = convert_href(text) + assert 'Link' in result + + +class TestParseEtaremune: + """Test etaremune list parsing.""" + + def test_parse_simple_list(self): + """Test parsing a simple etaremune list.""" + content = r''' +\begin{etaremune} +\item First item +\item Second item +\item Third item +\end{etaremune} +''' + items = parse_etaremune(content) + assert len(items) == 3 + assert 'First item' in items[0] + assert 'Second item' in items[1] + assert 'Third item' in items[2] + + def test_parse_list_with_formatting(self): + """Test parsing list with LaTeX formatting.""" + content = r''' +\begin{etaremune} +\item \textbf{Bold} text +\item \textit{Italic} text +\item \href{http://example.com}{Link} +\end{etaremune} +''' + items = parse_etaremune(content) + assert len(items) == 3 + assert 'Bold' in items[0] + assert 'Italic' in items[1] + assert 'Link' in items[2] + + def test_parse_empty_list(self): + """Test parsing content without etaremune.""" + content = 'No list here' + items = parse_etaremune(content) + assert len(items) == 0 + + def test_parse_multiline_items(self): + """Test parsing items that span multiple lines.""" + content = r''' +\begin{etaremune} +\item First line +continues here +\item Second item +\end{etaremune} +''' + items = parse_etaremune(content) + assert len(items) == 2 + assert 'First line' in items[0] + assert 'continues' in items[0] + + +class TestExtractHeaderInfo: + """Test header extraction.""" + + def test_extract_name(self): + """Test extracting name from header.""" + body = r''' +{\LARGE Jeremy R. Manning, \textsc{Ph.D.}}\\ +Director, Lab\\ +Department\\ +\section*{Employment} +Content here +''' + info = extract_header_info(body) + assert 'name' in info + assert 'Jeremy R. Manning' in info['name'] + + def test_extract_header_lines(self): + """Test extracting contact info lines.""" + body = r''' +{\LARGE Name}\\[0.25cm] +Department\\ +Email: \href{mailto:test@example.com}{test@example.com}\\ +\section*{Employment} +''' + info = extract_header_info(body) + assert 'header_lines' in info + assert len(info['header_lines']) > 0 + assert any('Department' in line for line in info['header_lines']) + + +class TestExtractSections: + """Test section extraction.""" + + def test_extract_single_section(self): + """Test extracting a single section.""" + body = r''' +Header content +\section*{Employment} +Employment details here +''' + sections = extract_sections(body) + assert len(sections) >= 1 + assert any(s.title == 'Employment' for s in sections) + + def test_extract_multiple_sections(self): + """Test extracting multiple sections.""" + body = r''' +\section*{Employment} +Job info +\section*{Education} +Degree info +\section*{Publications} +Paper list +''' + sections = extract_sections(body) + assert len(sections) >= 3 + titles = [s.title for s in sections] + assert 'Employment' in titles + assert 'Education' in titles + assert 'Publications' in titles + + def test_extract_subsections(self): + """Test extracting subsections.""" + body = r''' +\section*{Main Section} +Main content +\subsection*{Subsection 1} +Sub content 1 +\subsection*{Subsection 2} +Sub content 2 +''' + sections = extract_sections(body) + main_section = next((s for s in sections if s.title == 'Main Section'), None) + assert main_section is not None + assert len(main_section.subsections) >= 2 + + +class TestHTMLGeneration: + """Test HTML generation.""" + + def test_generate_basic_html(self): + """Test generating basic HTML structure.""" + tex_content = r''' +\documentclass{article} +\begin{document} +{\LARGE Test Name}\\ +Test Department\\ +\section*{Section} +Content here +\end{document} +''' + html = generate_html(tex_content) + + # Check structure + assert '' in html + assert '' in html + assert '' in html + assert '' in html + assert '' in html + assert '' in html + + def test_html_has_download_button(self): + """Test that HTML includes PDF download button.""" + tex_content = r''' +\documentclass{article} +\begin{document} +{\LARGE Test Name}\\ +\section*{Section} +Content +\end{document} +''' + html = generate_html(tex_content) + assert 'cv-download-bar' in html + assert 'Download CV as PDF' in html + + def test_html_has_css_link(self): + """Test that HTML includes CSS link.""" + tex_content = r''' +\documentclass{article} +\begin{document} +{\LARGE Test Name}\\ +\section*{Section} +Content +\end{document} +''' + html = generate_html(tex_content) + assert 'cv.css' in html + + def test_html_includes_sections(self): + """Test that HTML includes section content.""" + tex_content = r''' +\documentclass{article} +\begin{document} +{\LARGE Test Name}\\ +\section*{Employment} +Professor +\section*{Education} +Ph.D. +\end{document} +''' + html = generate_html(tex_content) + assert 'Employment' in html + assert 'Education' in html + assert 'Professor' in html + + def test_html_preserves_links(self): + """Test that links are preserved in HTML.""" + tex_content = r''' +\documentclass{article} +\begin{document} +{\LARGE Test Name}\\ +\href{mailto:test@example.com}{test@example.com}\\ +\section*{Section} +Visit \href{http://example.com}{our website} +\end{document} +''' + html = generate_html(tex_content) + assert 'mailto:test@example.com' in html + assert 'http://example.com' in html + assert '' in content + assert 'Jeremy R. Manning' in content + assert 'Employment' in content + + +class TestPDFCompilation: + """Test PDF compilation from LaTeX.""" + + def test_xelatex_available(self): + """Test that xelatex is available on the system.""" + result = subprocess.run(['which', 'xelatex'], capture_output=True) + if result.returncode != 0: + pytest.skip("xelatex not available on this system") + + def test_compile_minimal_pdf(self): + """Test compiling a minimal LaTeX document to PDF.""" + # Check xelatex availability + result = subprocess.run(['which', 'xelatex'], capture_output=True) + if result.returncode != 0: + pytest.skip("xelatex not available") + + with tempfile.TemporaryDirectory() as td: + temp_dir = Path(td) + + # Create minimal LaTeX file + tex_file = temp_dir / 'test.tex' + tex_file.write_text(r''' +\documentclass{article} +\begin{document} +Hello, World! +\end{document} +''') + + # Compile to PDF + cmd = ['xelatex', '-interaction=nonstopmode', 'test.tex'] + subprocess.run(cmd, cwd=temp_dir, capture_output=True) + + pdf_file = temp_dir / 'test.pdf' + assert pdf_file.exists(), "PDF was not created" + + # Check file size + size = pdf_file.stat().st_size + assert size > 1000, f"PDF too small: {size} bytes" + + def test_pdf_is_valid(self): + """Test that generated PDF starts with PDF magic bytes.""" + # Check xelatex availability + result = subprocess.run(['which', 'xelatex'], capture_output=True) + if result.returncode != 0: + pytest.skip("xelatex not available") + + with tempfile.TemporaryDirectory() as td: + temp_dir = Path(td) + + tex_file = temp_dir / 'test.tex' + tex_file.write_text(r''' +\documentclass{article} +\begin{document} +Test PDF content +\end{document} +''') + + cmd = ['xelatex', '-interaction=nonstopmode', 'test.tex'] + subprocess.run(cmd, cwd=temp_dir, capture_output=True) + + pdf_file = temp_dir / 'test.pdf' + + # Read first few bytes + with open(pdf_file, 'rb') as f: + header = f.read(5) + + assert header == b'%PDF-', f"Invalid PDF header: {header}" + + def test_pdf_reasonable_size(self): + """Test that PDF has reasonable file size (> 50KB for real CV).""" + # This test will use the actual CV file if it exists + if not TEX_FILE.exists(): + pytest.skip("JRM_CV.tex not found") + + # Check xelatex availability + result = subprocess.run(['which', 'xelatex'], capture_output=True) + if result.returncode != 0: + pytest.skip("xelatex not available") + + with tempfile.TemporaryDirectory() as td: + temp_dir = Path(td) + + # Copy CV to temp dir + import shutil + temp_tex = temp_dir / 'JRM_CV.tex' + shutil.copy(TEX_FILE, temp_tex) + + # Compile (may need fonts, so allow it to fail gracefully) + cmd = ['xelatex', '-interaction=nonstopmode', 'JRM_CV.tex'] + result = subprocess.run(cmd, cwd=temp_dir, capture_output=True) + + pdf_file = temp_dir / 'JRM_CV.pdf' + + if pdf_file.exists(): + size = pdf_file.stat().st_size + assert size > 50000, f"PDF too small: {size} bytes (expected > 50KB)" + + +class TestContentValidation: + """Test validation of generated content.""" + + def test_html_contains_name(self): + """Test that HTML contains Jeremy R. Manning.""" + if not TEX_FILE.exists(): + pytest.skip("JRM_CV.tex not found") + + with tempfile.TemporaryDirectory() as td: + temp_dir = Path(td) + html_file = temp_dir / 'cv.html' + + success = extract_cv(TEX_FILE, html_file) + assert success + + content = html_file.read_text() + assert 'Jeremy R. Manning' in content + + def test_html_contains_required_sections(self): + """Test that HTML contains required sections.""" + if not TEX_FILE.exists(): + pytest.skip("JRM_CV.tex not found") + + with tempfile.TemporaryDirectory() as td: + temp_dir = Path(td) + html_file = temp_dir / 'cv.html' + + success = extract_cv(TEX_FILE, html_file) + assert success + + content = html_file.read_text() + + # Required sections + required_sections = ['Employment', 'Education', 'Publications'] + for section in required_sections: + assert section in content, f"Missing section: {section}" + + def test_html_contains_email(self): + """Test that HTML contains contact email.""" + if not TEX_FILE.exists(): + pytest.skip("JRM_CV.tex not found") + + with tempfile.TemporaryDirectory() as td: + temp_dir = Path(td) + html_file = temp_dir / 'cv.html' + + success = extract_cv(TEX_FILE, html_file) + assert success + + content = html_file.read_text() + + # Should have email link + assert 'mailto:' in content + assert 'dartmouth.edu' in content + + def test_html_links_are_valid(self): + """Test that all links in HTML are well-formed URLs.""" + if not TEX_FILE.exists(): + pytest.skip("JRM_CV.tex not found") + + with tempfile.TemporaryDirectory() as td: + temp_dir = Path(td) + html_file = temp_dir / 'cv.html' + + success = extract_cv(TEX_FILE, html_file) + assert success + + content = html_file.read_text() + + # Find all href attributes + href_pattern = r'href="([^"]+)"' + hrefs = re.findall(href_pattern, content) + + assert len(hrefs) > 0, "No links found in HTML" + + # Check that each href is a valid URL or anchor + for href in hrefs: + # Should be http(s), mailto, tel, relative path, or anchor + assert (href.startswith('http://') or + href.startswith('https://') or + href.startswith('mailto:') or + href.startswith('tel:') or + href.startswith('#') or + href.endswith('.pdf') or + href.endswith('.html') or + '/' in href or # Relative paths + '.' in href), f"Invalid href: {href}" + + def test_html_has_proper_structure(self): + """Test that HTML has proper document structure.""" + with tempfile.TemporaryDirectory() as td: + temp_dir = Path(td) + tex_file = temp_dir / 'test.tex' + tex_file.write_text(r''' +\documentclass{article} +\begin{document} +{\LARGE Test Name}\\ +\section*{Section} +Content +\end{document} +''') + + html_file = temp_dir / 'test.html' + success = extract_cv(tex_file, html_file) + assert success + + content = html_file.read_text() + + # Check structure + assert content.startswith(''), "Missing DOCTYPE" + assert '' in content + assert '' in content + assert '' in content + assert '' in content + assert '' in content + assert '' in content + assert '' in content + assert '' in content + assert '' in content + + +class TestBuildCVIntegration: + """Integration tests for the full build process.""" + + def test_run_command(self): + """Test run_command function.""" + success, stdout, stderr = run_command(['echo', 'test']) + assert success + assert 'test' in stdout + + def test_run_command_timeout(self): + """Test run_command with timeout.""" + success, stdout, stderr = run_command(['sleep', '10'], timeout=1) + assert not success + assert 'timeout' in stderr.lower() or 'timed out' in stderr.lower() + + def test_cleanup_temp_files(self): + """Test cleanup of temporary LaTeX files.""" + with tempfile.TemporaryDirectory() as td: + temp_dir = Path(td) + + # Create fake temp files + extensions = ['.aux', '.log', '.out', '.synctex.gz'] + for ext in extensions: + (temp_dir / f'test{ext}').touch() + + # Monkey-patch DOCUMENTS_DIR + import build_cv + old_dir = build_cv.DOCUMENTS_DIR + build_cv.DOCUMENTS_DIR = temp_dir + + try: + cleanup_temp_files() + + # Check that files were removed + for ext in extensions: + assert not (temp_dir / f'test{ext}').exists() + finally: + build_cv.DOCUMENTS_DIR = old_dir + + def test_full_cv_build_if_files_exist(self): + """Test full CV build process if source files exist.""" + if not TEX_FILE.exists(): + pytest.skip("JRM_CV.tex not found") + + # Check xelatex availability + result = subprocess.run(['which', 'xelatex'], capture_output=True) + if result.returncode != 0: + pytest.skip("xelatex not available") + + # Save original files if they exist + import shutil + backup_pdf = None + backup_html = None + + if PDF_FILE.exists(): + backup_pdf = PDF_FILE.with_suffix('.pdf.backup') + shutil.copy(PDF_FILE, backup_pdf) + + if HTML_FILE.exists(): + backup_html = HTML_FILE.with_suffix('.html.backup') + shutil.copy(HTML_FILE, backup_html) + + try: + # Run build + success = build_cv() + + if success: + # Verify outputs + assert PDF_FILE.exists(), "PDF not created" + assert HTML_FILE.exists(), "HTML not created" + + # Check PDF is valid + with open(PDF_FILE, 'rb') as f: + header = f.read(5) + assert header == b'%PDF-', "Invalid PDF" + + # Check HTML has content + content = HTML_FILE.read_text() + assert len(content) > 10000, "HTML too small" + assert 'Jeremy R. Manning' in content + + finally: + # Restore backups + if backup_pdf and backup_pdf.exists(): + shutil.move(backup_pdf, PDF_FILE) + if backup_html and backup_html.exists(): + shutil.move(backup_html, HTML_FILE) + + +class TestRealCVContent: + """Tests using the actual CV file.""" + + def test_actual_cv_has_employment(self): + """Test that actual CV has Employment section.""" + if not TEX_FILE.exists(): + pytest.skip("JRM_CV.tex not found") + + content = read_latex_file(TEX_FILE) + assert r'\section*{Employment}' in content or r'\section{Employment}' in content + + def test_actual_cv_has_education(self): + """Test that actual CV has Education section.""" + if not TEX_FILE.exists(): + pytest.skip("JRM_CV.tex not found") + + content = read_latex_file(TEX_FILE) + assert r'\section*{Education}' in content or r'\section{Education}' in content + + def test_actual_cv_has_publications(self): + """Test that actual CV has Publications section.""" + if not TEX_FILE.exists(): + pytest.skip("JRM_CV.tex not found") + + content = read_latex_file(TEX_FILE) + assert r'\section*{Publications}' in content or r'\section{Publications}' in content + + def test_extract_cv_from_actual_file(self): + """Test extracting HTML from the actual CV LaTeX file.""" + if not TEX_FILE.exists(): + pytest.skip("JRM_CV.tex not found") + + with tempfile.TemporaryDirectory() as td: + temp_dir = Path(td) + html_file = temp_dir / 'JRM_CV.html' + + success = extract_cv(TEX_FILE, html_file) + assert success, "Failed to extract CV" + assert html_file.exists(), "HTML file not created" + + content = html_file.read_text() + + # Should be substantial (at least 10KB) + assert len(content) > 10000, f"HTML too small: {len(content)} bytes" + + # Should have structure + assert '' in content + assert '' in content + + # Should have key content + assert 'Jeremy R. Manning' in content + assert 'Employment' in content + assert 'Education' in content + assert 'Publications' in content + + # Should have download button + assert 'cv-download-bar' in content + assert 'Download CV as PDF' in content + + +class TestEdgeCases: + """Test edge cases and error handling.""" + + def test_extract_cv_nonexistent_file(self): + """Test extracting from nonexistent file.""" + with tempfile.TemporaryDirectory() as td: + temp_dir = Path(td) + nonexistent = temp_dir / 'nonexistent.tex' + output = temp_dir / 'output.html' + + # Should handle gracefully + success = extract_cv(nonexistent, output) + assert not success + + def test_balanced_braces_no_closing(self): + """Test balanced_braces_extract with no closing brace.""" + text = '{unclosed' + content, end = balanced_braces_extract(text, 0) + assert content is None + assert end == -1 + + def test_balanced_braces_not_starting_with_brace(self): + """Test balanced_braces_extract when not starting with brace.""" + text = 'no brace here' + content, end = balanced_braces_extract(text, 0) + assert content is None + assert end == -1 + + def test_convert_latex_empty_string(self): + """Test converting empty string.""" + result = convert_latex_formatting('') + assert result == '' + + def test_parse_etaremune_malformed(self): + """Test parsing malformed etaremune.""" + content = r'\begin{etaremune}' # No end tag + items = parse_etaremune(content) + assert len(items) == 0 + + def test_extract_document_body_no_document(self): + """Test extracting body when no document environment.""" + latex = 'Just some text' + body = extract_document_body(latex) + assert body == latex # Returns original if no document env found + + def test_generate_html_minimal_document(self): + """Test generating HTML from minimal document.""" + tex = r''' +\documentclass{article} +\begin{document} +{\LARGE Test Name}\\ +\section*{Test Section} +Minimal content here +\end{document} +''' + html = generate_html(tex) + assert '' in html + # Content appears in sections, check for section title instead + assert 'Test Section' in html or 'Test Name' in html From 0a3cee59af3bc09cb80475729d51ae759a284781 Mon Sep 17 00:00:00 2001 From: Jeremy Manning Date: Sun, 14 Dec 2025 14:53:22 -0500 Subject: [PATCH 3/4] Improve CV HTML formatting to match PDF layout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix header spacing: reduce name-title gap, add space after Director and USA - Add block spacers for text block separation (matching LaTeX \\[Xcm]) - Extract and display footnotes as section notes under headers - Make Undergraduate Advisees section 2-column layout - Fix sub-heading weights: h3/h4 now normal weight (matching LaTeX \mdseries) - Reduce paragraph margins for tighter spacing - Update download bar structure to match CSS expectations - Remove duplicate "Last updated" text 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- css/cv.css | 110 +++++++----- documents/JRM_CV.html | 385 ++++++++++++++++++++++++++++++++++++++--- documents/JRM_CV.pdf | Bin 105539 -> 105533 bytes scripts/extract_cv.py | 255 ++++++++++++++++++++++----- tests/test_build_cv.py | 8 +- 5 files changed, 645 insertions(+), 113 deletions(-) diff --git a/css/cv.css b/css/cv.css index fbe078e..a6166f0 100644 --- a/css/cv.css +++ b/css/cv.css @@ -41,8 +41,8 @@ ========================================================================== */ :root { - --primary-green: rgb(0, 112, 60); - --bg-green: rgba(0, 112, 60, 0.2); + --primary-green: rgb(0, 105, 62); + --bg-green: rgba(0, 105, 62, 0.2); --dark-text: rgba(0, 0, 0, 0.7); --light-gray: #f5f5f5; --border-gray: #e0e0e0; @@ -62,7 +62,7 @@ body { font-family: 'Dartmouth Ruzicka', Georgia, serif; font-size: 11pt; - line-height: 1.6; + line-height: 1.35; color: var(--dark-text); background-color: white; padding-top: 60px; /* Space for sticky download bar */ @@ -137,59 +137,64 @@ body { ========================================================================== */ .cv-header { - text-align: center; - margin-bottom: 3rem; - padding-bottom: 2rem; - border-bottom: 2px solid var(--primary-green); + text-align: left; + margin-bottom: 1.5rem; + padding-bottom: 0; + border-bottom: none; } .cv-header h1 { - font-size: 2.5rem; - font-weight: bold; - color: var(--primary-green); - margin-bottom: 1rem; - letter-spacing: 0.5px; + font-size: 17pt; + font-weight: normal; + color: var(--dark-text); + margin-bottom: 0; + letter-spacing: 0; } .cv-header .contact-info { - font-size: 0.95rem; - line-height: 1.8; + font-size: 11pt; + line-height: 1.3; color: var(--dark-text); } .cv-header .contact-info p { - margin: 0.25rem 0; + margin: 0; +} + +.cv-header .contact-info p.space-after { + margin-bottom: 0.5rem; } /* Headings ========================================================================== */ h2 { - font-size: 1.4rem; - font-weight: bold; - color: var(--primary-green); - margin-top: 2rem; - margin-bottom: 1rem; - padding-bottom: 0.5rem; - border-bottom: 1px solid var(--border-gray); - text-transform: uppercase; - letter-spacing: 1px; + font-size: 1.3rem; + font-weight: normal; + color: var(--dark-text); + margin-top: 2.5rem; + margin-bottom: 0.75rem; + padding-bottom: 0; + border-bottom: none; + text-transform: none; + letter-spacing: 0; } h3 { - font-size: 1.1rem; - font-weight: bold; + font-size: 1.05rem; + font-weight: normal; color: var(--dark-text); margin-top: 1.5rem; - margin-bottom: 0.75rem; + margin-bottom: 0.5rem; } h4 { font-size: 1rem; - font-weight: bold; + font-weight: normal; + font-style: italic; color: var(--dark-text); - margin-top: 1rem; - margin-bottom: 0.5rem; + margin-top: 0.75rem; + margin-bottom: 0.25rem; } /* Lists @@ -201,41 +206,47 @@ ul, ol { } li { - margin-bottom: 0.75rem; - line-height: 1.6; + margin-bottom: 0.5rem; + line-height: 1.35; } /* Reverse-numbered lists for publications and awards */ +/* Use native browser support for reversed lists */ ol[reversed] { - list-style: none; - counter-reset: item; + list-style-position: outside; + padding-left: 2.5rem; } ol[reversed] > li { - counter-increment: item -1; - position: relative; - padding-left: 2.5rem; + padding-left: 0.5rem; } -ol[reversed] > li::before { - content: counter(item) "."; - position: absolute; - left: 0; - font-weight: bold; - color: var(--primary-green); - min-width: 2rem; - text-align: right; - padding-right: 0.5rem; +ol[reversed] > li::marker { + font-weight: normal; + color: var(--dark-text); } /* Paragraphs ========================================================================== */ p { - margin-bottom: 1rem; + margin-bottom: 0.25rem; + margin-top: 0; text-align: justify; } +/* Reduce spacing for paragraphs in section content (like Advisor/Dissertation lines) */ +section p { + margin-bottom: 0.1rem; +} + +/* Section notes (from footnotes) */ +.section-note { + font-size: 0.9rem; + margin-bottom: 0.75rem; + color: var(--dark-text); +} + /* Links ========================================================================== */ @@ -270,6 +281,11 @@ strong, b { font-weight: bold; } +.block-spacer { + display: block; + height: 0.4rem; +} + /* Two-Column Lists ========================================================================== */ diff --git a/documents/JRM_CV.html b/documents/JRM_CV.html index 9303408..ddfceb4 100644 --- a/documents/JRM_CV.html +++ b/documents/JRM_CV.html @@ -8,19 +8,22 @@
    - Download CV as PDF +
    + Curriculum Vitae + Download PDF +

    Jeremy R. Manning, Ph.D.

    -

    Director, Contextual Dynamics Laboratory

    +

    Director, Contextual Dynamics Laboratory

    Department of Psychological and Brain Sciences

    Dartmouth College

    HB 6207, Moore Hall

    Hanover, NH 03755

    -

    U.S.A.

    +

    U.S.A.

    Email: jeremy.r.manning@dartmouth.edu

    Phone: 603.646.2777

    URL: @@ -34,12 +37,12 @@

    Employment

    Dartmouth College, Hanover, NH (2024 – )

    Department of Psychological and Brain Sciences

    Additional affiliation: Cognitive Science

    -

    Tenured: 2024

    +

    Tenured: 2024

    Assistant Professor, Dartmouth College, Hanover, NH (2015 – 2024)

    Department of Psychological and Brain Sciences

    Additional affiliation: Cognitive Science

    -

    Reappointed: 2018

    +

    Reappointed: 2018

    Postdoctoral Research Associate, Princeton University, Princeton, NJ (2011 – 2015)

    Princeton Neuroscience @@ -54,12 +57,12 @@

    Education

    Philadelphia, PA (2011)

    Advisor: Michael Kahana, Ph.D.

    Dissertation: Acquisition, storage, and - retrieval in digital and biological brains

    + retrieval in digital and biological brains

    B.S. in Neuroscience (High honors, Magna cum laude), Brandeis University, Waltham, MA (2006)

    Advisor: Robert Sekuler, Ph.D.

    Dissertation: Modeling human spatial navigation using a - degraded ideal navigator

    + degraded ideal navigator

    B.S. in Computer Science (Magna cum laude), Brandeis University, Waltham, MA (2006) @@ -144,12 +147,160 @@

    Grants, honors, and awards (selected)

    Publications

    - -
    -

    Book chapters

    -

    \end{etaremune}

    +

    Undergraduate trainees are denoted by + underlined text, graduate trainees are indicated by italicized text, and postdoctoral trainees are indicated by underlined and italicized text.

    +
      +
    1. Fitzpatrick PC, Heusser AC, Manning JR (2025) Text embedding models yield +high-resolution insights into conceptual knowledge from short multiple-choice +quizzes. Nature Communications: In press.
    2. +
    3. Stropkay HF, Chen J, Latifi MJ, Rockmore DN, Manning JR (2025)) A stylometric application of large language models. arXiv: 2510.21958.
    4. +
    5. Manning JR (2025) Why we're so preoccupied by the past. Scientific American, online.
    6. +
    7. Owen LLW, Manning JR (2024) High-level cognition is supported by +information-rich but compressible brain activity +patterns. Proceedings of the National Academy of Sciences, USA, 121(35): e2400082121.
    8. +
    9. Xu X, Zhu Z, Zheng X, Manning JR (2024) Temporal +asymmetries in inferring unobserved past and future events. Nature +Communications, 15: 8502.
    10. +
    11. Jolly E, Sadhukha S, Iqbal M, Molani Z, Walsh T, Manning JR, + Chang LJ (2023) People are represented and remembered through their + relationships with others. PsyArXiv: bw9r2.
    12. +
    13. Ziman K, Lee MR, Martinez AR, Manning JR (2023) Category-based +and location-based volitional covert attention are mediated by +different mechanisms and affect memory at different timescales. +PsyArXiv: 2ps6e.
    14. +
    15. Manning JR, Whitaker EC, Fitzpatrick PC, Lee MR, Frantz AM, +Bollinger BJ, Romanova D, Field CE, Heusser AC (2023) Feature and order +manipulations in a free recall task affect memory for current and future lists. +PsyArXiv: erzfp. (Under second round of reviews at Psychological Review)
    16. +
    17. Fitzpatrick PC, Manning JR (2023) +davos: a Python package "smuggler" for constructing +lightweight reproducible notebooks. SoftwareX: in press.
    18. +
    19. Manning JR (2023) Context reinstatement. In Kahana MJ and +Wagner AD, Ed. Handbook of Human Memory. New York, NY: +Oxford University Press. Chapter 38.
    20. +
    21. Manning JR (2023) Identifying +stimulus-driven neural activity patterns in multi-patient intracranial recordings. +In Axmacher N, Ed. Intracranial EEG for Cognitive + Neuroscience. New York, NY: Springer. Chapter 48.
    22. +
    23. Manning JR, Notaro GM, Chen E, Fitzpatrick PC +(2022) Fitness tracking reveals task-specific associations between +memory, mental health, and physical activity. Scientific + Reports, 12: 13822.
    24. +
    25. Kumar M, Anderson MJ, Antony JW, Baldassano C, Brooks PP, Cai MB, +Chen P-HC, Ellis CT, Henselman-Petrusek G, Huberdeau D, Hutchinson BJ, +Li PY, Lu Q, Manning JR, Mennen AC, Nastase SA, Richard H, +Schapiro AC, Schuck NW, Suo D, Turek JS, Vo VA, Wallace G, Wang Y, +Zhang H, Zhu X, Capotă M, Cohen JD, Hasson U, Li K, Ramadge PJ, +Turk-Browne NB, Willke TL, Norman KA (2022) BrainIAK: the brain +imaging analysis kit. Aperture, 1(4): 1–19.
    26. +
    27. Scangos KW, Khambhati AN, Daly PM, Owen LLW, +Manning JR, Ambrose JB, Austin E, Dawes HE, Krystal AD, Chang +EG (2021) Distributed subnetworks of depression defined by direct +intracranial neurophysiology. Frontiers in Human + Neuroscience, 15: doi.org/10.3389/fnhum.2021.746499.
    28. +
    29. Chen HT, Manning JR, van der Meer MAA (2021) +Between-subject prediction reveals a shared representational geometry +in the rodent hippocampus. Current Biology, 31: +1–12.
    30. +
    31. Owen LLW, Chang TH, Manning JR (2021) High-level +cognition during story listening is reflected in high-order dynamic +correlations in neural activity patterns. Nature + Communications, 12(5728): doi.org/10.1038/s41467-021-25876-x.
    32. +
    33. Manning JR (2021) Episodic memory: mental time travel or a +quantum 'memory wave' function? Psychological Review, 128(4): +711–725.
    34. +
    35. Chang LJ, Jolly E, Cheong JH, +Rapuano K, Greenstein N, Chen PHA, Manning JR (2021) +Endogenous variation in ventromedial prefrontal cortex state dynamics +during naturalistic viewing reflects affective +experience. Science Advances, 7(17): eabf7129.
    36. +
    37. Xie T, Cheong JH, Manning JR, Brandt AM, Aronson +JP, Jobst BC, Bujarski KA, Chang LJ (2021) Minimal functional +alignment of ventromedial prefrontal cortex intracranial EEG signals +during naturalistic viewing. bioRxiv: 443308.
    38. +
    39. Ziman K, Manning JR (2021) Unexpected false feelings of +familiarity about faces are associated with increased pupil +dilations. bioRxiv: 432360.
    40. +
    41. Heusser AC, Fitzpatrick PC, Manning JR (2021) Geometric models reveal +behavioral and neural signatures of transforming naturalistic experiences into +episodic memories. Nature Human Behaviour: +doi.org/10.1038/s41562-021-01051.
    42. +
    43. Owen LLW, Muntianu TA, Heusser AC, Daly P, Scangos K, +Manning JR (2020) A Gaussian process model of human +electrocorticographic data. {\em Cerebral Cortex}, 30(10): +5333–5345.
    44. +
    45. Chang L, Manning JR, Baldassano C, de la Vega A, Fleetwood G, +Geerligs L, Haxby J, Lahnakoski J, Parkinson C, Shappell H, Shim WM, +Wager T, Yarkoni T, Yeshurun Y, Finn E (2020) Naturalistic data +analysis: doi.org/10.5281/zenodo.3937849.
    46. +
    47. Heusser AC, Ziman K, Owen LLW, Manning JR (2018) +HyperTools: a Python toolbox for gaining geometric insights into +high-dimensional data. {\em Journal of Machine Learning Research}, 18: +1–6.
    48. +
    49. Ziman K, Heusser AC, Fitzpatrick PC, Field CE, Manning JR (2018) Is +automatic speech-to-text transcription ready for use in psychological +experiments? {\em Behavior Research Methods}: +doi.org/10.3758/s13428-018-1037-4.
    50. +
    51. Heusser AC, Manning JR (2018) Capturing the geometric structure +of episodic memories for naturalistic experiences. Conference on +Cognitive Computational Neuroscience: +doi.org/10.32470/CCN.2018.1267-0.
    52. +
    53. Manning JR, Zhu X, Willke TL, Ranganath R, Stachenfeld K, Hassan U, +Blei DM, Norman KA (2018) A probabilistic approach to discovering dynamic +full-brain functional connectivity patterns. {\em NeuroImage}, 180: +243–252.
    54. +
    55. Heusser AC, Fitzpatrick PC, Field CE, Ziman K, Manning JR +(2017) Quail: a Python toolbox for analyzing and plotting free recall data. +{\em The Journal of Open Source Software}, 2(18): 424.
    56. +
    57. Manning JR, Hulbert JC, Williams J, Piloto L, Sahakyan L, +Norman KA (2016) A neural signature of contextually mediated intentional +forgetting. {\em Psychonomic Bulletin and Review}, 23(5): 1534–1542.
    58. +
    59. Anderson MJ, Capota M, Turek JS, Zhu X, Willke TL, Wang Y, Chen P-H, +Manning JR, Ramadge PJ, Norman KA (2016) Enabling factor analysis on +thousand-subject neuroimaging datasets. IEEE Xplore, International +Conference on Big Data (BigData 2016): +doi.org/10.1109/BigData.2016.7840719.
    60. +
    61. Benson NC, Manning JR, Brainard DH (2014) Unsupervised +learning of cone spectral classes from natural images. {\em PLoS Computational +Biology}, 10(6): e1003652.
    62. +
    63. Manning JR, Ranganath R, Norman KA, Blei DM (2014) Topographic factor +analysis: a Bayesian model for inferring brain networks from neural data. {\em +PLoS One}, 9(5): e94914.
    64. +
    65. Manning JR, Lew TF, Li N, Kahana MJ, Sekuler RW (2014) +MAGELLAN: a cognitive map-based model of human wayfinding. {\em +Journal of Experimental Psychology: General}, 143(3): 1314–1330.
    66. +
    67. Manning JR, Ranganath R, Keung W, Turk-Browne N, Cohen +JD, Norman KA, Blei DM (2014) Hierarchical Topographic Factor Analysis. +IEEE Xplore, 4th International Workshop on Pattern +Recognition in Neuroimaging: doi.org/10.1109/PRNI.2014.6858530.
    68. +
    69. Manning JR, Kahana MJ, Norman KA (2014) The role of +context in memory. In Gazzaniga M, Ed. The Cognitive Neurosciences, +Fifth Edition. Cambridge, MA: MIT Press. Chapter 47.
    70. +
    71. Manning JR, Kahana MJ (2012) Interpreting semantic +clustering effects in free recall. Memory, 20(5): 511–517.
    72. +
    73. Manning JR, Sperling MR, Sharan A, Rosenberg EA, Kahana MJ (2012) +Spontaneously reactivated patterns in frontal and temporal lobe predict +semantic clustering during memory search. The Journal of Neuroscience, +32(26): 8800–8816.
    74. +
    75. Manning JR, Gershman SJ, Norman KA, Blei DM (2012) Factor +topographic latent source analysis: factor analysis for brain images. +Neural Information Processing Systems (NeurIPS) Workshop on Machine +Learning and Interpretation in Neuroimaging, 2: Online.
    76. +
    77. Manning JR, Polyn SM, Baltuch G, Litt B, Kahana MJ (2011) +Oscillatory patterns in temporal lobe reveal context reinstatement during +memory search. Proceedings of the National Academy of Sciences of the +United States of America, 108(31): 12893–12897.
    78. +
    79. Jacobs J, Manning JR, Kahana MJ (2010) Response to +Miller: "broadband" vs. "high gamma" electrocorticographic signals. +The Journal of Neuroscience, 30(19): Online.
    80. +
    81. Manning JR, Jacobs J, Fried I, Kahana MJ (2009) Broadband +shifts in local field potential power spectra are correlated with single-neuron +spiking in humans. The Journal of Neuroscience, 29(43): 13613–13620.
    82. +
    83. Manning JR, Brainard DH (2009) Optimal design of photoreceptor +mosaics: why we do not see color at night. Visual Neuroscience, 26: +5–19.
    84. +
    -
    @@ -283,24 +434,213 @@

    Open courses (selected)

  1. Computational Neuroscience; doi.org/10.5281/zenodo.10235877
- -
-

Dartmouth College

-

Mentorship (selected)

-
    +

    Senior thesis students are denoted by asterisks + (*)

    +

    Postdoctoral Advisees

    +
    1. Hung-Tu Chen (2024 – 2025; current position: Meta)
    2. Gina Notaro (2017 – 2018; current position: HRL Laboratories)
    3. Andrew Heusser (2016 – 2018; current position: PyMC Labs)
    +

    Graduate Advisees

    +
      +
    1. Paxton Fitzpatrick (Doctoral student; 2021 – )
    2. +
    3. Xinming Xu (Doctoral student; 2021 – )
    4. +
    5. Mark Taylor (Masters student, Quantitative Biomedical Sciences; 2021)
    6. +
    7. Caroline Lee (Doctoral student; 2019 – 2021)
    8. +
    9. Max Bluestone (Masters student, Quantitative Biomedical +Sciences; 2018 – 2020)
    10. +
    11. Deepanshi Shokeen (Masters student, Quantitative Biomedical +Sciences; 2018 – 2020)
    12. +
    13. Kirsten Ziman (Doctoral student; 2017 – 2022; current position: Postdoctoral researcher at Princeton University)
    14. +
    15. Lucy Owen (Doctoral student; 2016 – 2021; current position: Assistant Professor at University of Montana)
    16. +
    17. Tom Hao Chang (Masters student, Computer Science; co-advised with +Qiang Liu; 2016 – 2017; current position: Robinhood)
    18. +
    19. Hanli Li (Masters student, Computer Science; co-advised with Qiang +Liu; 2016)
    20. +
    +

    Thesis Committees

    +
      +
    1. Jane Han (Advisor: James Haxby)
    2. +
    3. Lindsey Tepfer (Advisor: Mark Thornton)
    4. +
    5. Arati Sharma (Advisor: Kate Nautiyal)
    6. +
    7. Clara Sava-Segal (Advisor: Emily Finn)
    8. +
    9. Manish Mohapatra (Advisor: Matthijs van der Meer; Graduated 2025)
    10. +
    11. Megan Hillis (Advisor: David Kraemer; Graduated 2025)
    12. +
    13. Omri Raccah (Advisor: David Poeppel; Graduated 2024)
    14. +
    15. Courtney Jiminez (Advisor: Meghan Meyer; Graduated 2024)
    16. +
    17. Hung-tu Chen (Advisor: Matthijs van der Meer; Graduated 2024)
    18. +
    19. Dhaval Bhatt (Advisor: Meghan Meyer; Graduated 2023)
    20. +
    21. Tiankang Xie (Advisor: Luke Chang; Graduated 2023)
    22. +
    23. Vassiki Chauhan (Advisors: Ida Gobbini and James Haxby; Graduated 2021)
    24. +
    25. Emily Irvine (Advisor: Matthijs van der Meer; Graduated 2020)
    26. +
    27. Eli Bowen (Advisor: Richard Granger; Graduated 2020)
    28. +
    29. Eshin Jolly (Advisor: Luke Chang; Graduated 2020)
    30. +
    31. Stephen Meisenhelter (Advisor: Barbara Jobst; Graduated 2020)
    32. +
    33. Feilong Ma (Advisor: James Haxby; Graduated 2019)
    34. +
    35. Kevin Hartstein (Advisor: Peter Tse; Graduated 2019)
    36. +
    37. Beau Sievers (Advisor: Thalia Wheatley; Graduated 2018)
    38. +
    39. Kristina Rapuano (Advisor: Luke Chang; Graduated 2018)
    40. +
    41. Luke Eglington (Advisor: Sean Kang; Graduated 2018)
    42. +
    43. Gina Notaro (Advisor: Solomon Diamond; Graduated 2017)
    44. +
    +

    Specialist Committees

    +
      +
    1. Yuqi Zhang (Advisors: Richard Granger and James Haxby)
    2. +
    3. Covert Geary (Advisor: John Murray)
    4. +
    5. Menghan Yang (Advisor: Luke Chang)
    6. +
    7. Deepasri Prasad (Advisor: Caroline Robertson)
    8. +
    9. Zizhuang Miao (Advisor: Tor Wager)
    10. +
    11. Benjamin Graul (Advisor: Tor Wager)
    12. +
    13. Yeongji Lee (Advisor: David Kraemer)
    14. +
    15. Thomas Botch (Advisors: Emily Finn and Caroline Robertson)
    16. +
    17. Dhaval Bhatt (Advisor: Meghan Meyer)
    18. +
    19. Clara Sava-Segal (Advisor: Emily Finn)
    20. +
    21. Wasita Mahaphanit (Advisor: Luke Chang)
    22. +
    23. Jane Han (Advisor: James Haxby)
    24. +
    25. Megan Hillis (Advisor: Caroline Robertson)
    26. +
    27. Anna Mynick (Advisor: Caroline Robertson)
    28. +
    29. Marissa Clark (Advisor: Luke Chang)
    30. +
    31. Robert Quon (Advisor: Barbara Jobst)
    32. +
    33. Mira Nencheva (Advisor: Casey Lew-Williams)
    34. +
    35. Marvin Maechler (Advisor: Peter Tse)
    36. +
    37. Eli Bowen (Advisor: Richard Granger)
    38. +
    39. Emma Templeton (Advisor: Thalia Wheatley)
    40. +
    41. Feilong Ma (Advisor: James Haxby)
    42. +
    43. Youki Tanaka (Advisor: Matthijs van der Meer)
    44. +
    +

    Undergraduate Advisees

    +
      +
    1. Sam Haskel* (2025 – )
    2. +
    3. Harrison Stropkay* (co-advised with Daniel Rockmore; 2025)
    4. +
    5. Kevin Chang (2025 – )
    6. +
    7. Andrew Richardson (2025 – )
    8. +
    9. Ben Hanson (2025 – )
    10. +
    11. Annabelle Morrow (2025 – )
    12. +
    13. Owen Phillips (2025 – )
    14. +
    15. Rodrigo Vega Ayllon (2025 – )
    16. +
    17. Joy Maina (2025 – )
    18. +
    19. Alexandra Wingo (2025 – )
    20. +
    21. Angelyn Liu (2025 – )
    22. +
    23. Miel Wewerka (2024 – )
    24. +
    25. Manraaj Singh (2024 – )
    26. +
    27. Can Kam (2024 – )
    28. +
    29. Chelsea Joe (2024 – )
    30. +
    31. Jacob Bacus (2024 – )
    32. +
    33. Rohan Goyal (2024 – )
    34. +
    35. Harrison Stropkay* (2024 – )
    36. +
    37. Abigayle McCusker (2024 – )
    38. +
    39. Torsha Chakraverty (2024 – )
    40. +
    41. Chloe Terestchenko (2024 – )
    42. +
    43. Ansh Motiani (2024 – )
    44. +
    45. Kaitlyn Peng* (2024 – )
    46. +
    47. Everett Tai (2024 – )
    48. +
    49. Andrew Cao (2024 – )
    50. +
    51. Michael Chen (2023 – )
    52. +
    53. Jake McDermid (2023 – )
    54. +
    55. Om Shah (2023 – )
    56. +
    57. Grady Redding (2023 – )
    58. +
    59. DJ Matusz (2023 – )
    60. +
    61. Sarah Parigela (2023 – )
    62. +
    63. Aaryan Agarwal (2023 – )
    64. +
    65. Maura Hough (2023 – )
    66. +
    67. Emma Reeder (2023 – )
    68. +
    69. Safwan Rashid (2023 )
    70. +
    71. Francisca Fadairo (2023 – )
    72. +
    73. Ameer Talha Yasser (2023)
    74. +
    75. Yue Zhuo (2023 – )
    76. +
    77. Megan Liu (2023 – 2024)
    78. +
    79. Charles Baker (2023)
    80. +
    81. Andrew Shi (2023)
    82. +
    83. Ash Chinta (2023)
    84. +
    85. Xueyao Zheng (2023)
    86. +
    87. Sergio Campos Legonia (2023)
    88. +
    89. Jennifer Xu (2023 – )
    90. +
    91. Elias Emery (2023)
    92. +
    93. Yvonne Chen (2023)
    94. +
    95. William McCall (2023)
    96. +
    97. Natalie Schreder (2023)
    98. +
    99. Raselas Dessalegn (2023)
    100. +
    101. Grace Wang (2023)
    102. +
    103. Mira Chiruvolu (2023 – 2024)
    104. +
    105. Anna Mikhailova (2022)
    106. +
    107. Ansh Patel (2022 – )
    108. +
    109. Ziyan Zhu (2022 – )
    110. +
    111. Benjamin Lehrburger (2022)
    112. +
    113. Thomas Corrado (2022)
    114. +
    115. Samuel Crombie (2022)
    116. +
    117. Alexander Marcoux (2022)
    118. +
    119. Jessna Brar (2022)
    120. +
    121. Wenhua Liang (2022)
    122. +
    123. Kevin Cao (2022)
    124. +
    125. Goutham Veeramachaneni (2022)
    126. +
    127. Zachary Somma (2022)
    128. +
    129. Dawson Haddox (2022)
    130. +
    131. Swestha Jain (2022)
    132. +
    133. Aidan Adams (2021)
    134. +
    135. Damini Kohli (2021)
    136. +
    137. Kunal Jha* (2021 – )
    138. +
    139. Daniel Carstensen* (2021 – )
    140. +
    141. Brian Chiang (2021 – 2022)
    142. +
    143. Daniel Ha (2021)
    144. +
    145. Darren Gu (2020 – 2021)
    146. +
    147. Tyler Chen (2020 – 2022)
    148. +
    149. Tehut Biru* (2020 – 2021)
    150. +
    151. Chris Suh (2020 – 2021)
    152. +
    153. Helen Liu (2020)
    154. +
    155. Kelly Rutherford (2020)
    156. +
    157. Chris Jun (2020 – 2022)
    158. +
    159. Ethan Adner (2020 – 2022)
    160. +
    161. Chris Long (2020 – 2021)
    162. +
    163. Esme Chen (2020 – 2021)
    164. +
    165. Luca Lit (2020)
    166. +
    167. Vivian Tran (2020)
    168. +
    169. Greg Han (2020)
    170. +
    171. Austin Zhang (2020)
    172. +
    173. Chelsea Uddenberg (2020)
    174. +
    175. Shane Hewitt (2020)
    176. +
    177. Chetan Palvuluri (2020)
    178. +
    179. Aaron Lee (2019 – 2020)
    180. +
    181. Anne George (2019 – 2020)
    182. +
    183. Sarah Park (2019 – 2020)
    184. +
    185. Shane Park (2019 – 2020)
    186. +
    187. William Chen (2019 – 2020)
    188. +
    189. Tudor Muntianu (2019 – 2021)
    190. +
    191. William Baxley (2018 – 2019)
    192. +
    193. Ann Carpenter (2018)
    194. +
    195. Seung Ju Lee (2018)
    196. +
    197. Mustafa Nasir-Moin (2018)
    198. +
    199. Iain Sheerin (2018)
    200. +
    201. Darya Romanova (2018)
    202. +
    203. Alejandro Martinez (2018 – 2020)
    204. +
    205. Rachael Chacko (2018)
    206. +
    207. Kirsten Soh (2018)
    208. +
    209. Paxton Fitzpatrick* (2017 – 2019)
    210. +
    211. Stephen Satterthwaite (2017 – 2018)
    212. +
    213. Bryan Bollinger (2017 – 2018)
    214. +
    215. Christina Lu (2017)
    216. +
    217. Armando Oritz (2017)
    218. +
    219. Campbell Field (2016 – 2018)
    220. +
    221. Madeline Lee (2016 – 2020)
    222. +
    223. Wei Liang Samuel Ching (2016 – 2017)
    224. +
    225. Marisol Tracy (2016 – 2017)
    226. +
    227. Allison Frantz (2016 – 2017)
    228. +
    229. Aamuktha Porika (2016 – 2017)
    230. +
    231. Jake Rost (2016)
    232. +
    233. Clara Silvanic (2016)
    234. +
    235. Aman Agarwal (2016)
    236. +
    237. Joseph Finkelstein (2016)
    238. +
    239. Sheherzad Mohydin (2016)
    240. +
    241. Peter Tran (2016)
    242. +
    243. Gal Perlman (2016)
    244. +
    245. Jessica Tin (2016)
    246. +
    +
-
-

Brandeis University

- -
@@ -368,9 +708,6 @@

Ad-hoc reviewer

Society for Artificial Intelligence and Statistics (AISTATS), Swiss National Science Foundation, The Journal of Neuroscience -

\begin{center} -{\scriptsize Last updated: \today} -\end{center}

diff --git a/documents/JRM_CV.pdf b/documents/JRM_CV.pdf index 3f343cb4aca85a7be562a2d6c2a5dded2287d358..eaf5e4615b8b07571d32309ca925ae1536b48565 100644 GIT binary patch delta 5120 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
str: return ''.join(result) +def remove_command_with_braces(text: str, cmd: str) -> str: + """Remove a LaTeX command and its braced argument, handling nested braces.""" + pattern = '\\' + cmd + '{' + result = [] + i = 0 + + while i < len(text): + pos = text.find(pattern, i) + if pos == -1: + result.append(text[i:]) + break + + result.append(text[i:pos]) + brace_pos = pos + len(pattern) - 1 + _, end_pos = balanced_braces_extract(text, brace_pos) + + if end_pos > 0: + i = end_pos + else: + result.append(text[pos:pos + len(pattern)]) + i = pos + len(pattern) + + return ''.join(result) + + def convert_latex_formatting(text: str) -> str: """Convert LaTeX formatting commands to HTML.""" # Remove LaTeX comments (lines starting with %) text = re.sub(r'^%.*$', '', text, flags=re.MULTILINE) text = re.sub(r'(? str: for old, new in replacements: text = text.replace(old, new) - # Line breaks with spacing - text = re.sub(r'\\\\\[[\d.]+cm\]', '
\n', text) + # Line breaks with spacing - \\[.1cm] etc adds extra space between blocks + text = re.sub(r'\\\\\[[\d.]+cm\]', '
\n', text) text = text.replace(r'\\', '
\n') - # Remove commands we don't need - text = re.sub(r'\\blfootnote\{[^}]*\}', '', text) - text = re.sub(r'\\vspace\{[^}]*\}', '', text) - text = re.sub(r'\\hspace\{[^}]*\}', '', text) + # Remove remaining LaTeX commands we don't need text = re.sub(r'\\noindent\s*', '', text) # Math mode: $...$ - simple handling @@ -185,20 +212,18 @@ def convert_latex_formatting(text: str) -> str: text = re.sub(r'\^\\mathrm\{([^}]+)\}', r'\1', text) text = re.sub(r'\^\{([^}]+)\}', r'\1', text) + # Remove any remaining raw LaTeX commands that shouldn't appear + text = re.sub(r'\\begin\{center\}', '', text) + text = re.sub(r'\\end\{center\}', '', text) + text = re.sub(r'\{\\scriptsize\s+', '', text) + text = re.sub(r'\\today\}?\s*', '', text) + return text -def parse_etaremune(content: str) -> List[str]: - """Parse etaremune environment (reverse-numbered list) and return items.""" +def parse_single_etaremune(list_content: str) -> List[str]: + """Parse a single etaremune list content and return items.""" items = [] - - # Find etaremune content - match = re.search(r'\\begin\{etaremune\}(.+?)\\end\{etaremune\}', content, re.DOTALL) - if not match: - return items - - list_content = match.group(1) - # Split by item parts = re.split(r'\\item\s*', list_content) @@ -210,6 +235,18 @@ def parse_etaremune(content: str) -> List[str]: return items +def parse_etaremune(content: str) -> List[str]: + """Parse etaremune environment (reverse-numbered list) and return items.""" + items = [] + + # Find ALL etaremune lists + for match in re.finditer(r'\\begin\{etaremune\}(.+?)\\end\{etaremune\}', content, re.DOTALL): + list_content = match.group(1) + items.extend(parse_single_etaremune(list_content)) + + return items + + def parse_multicol_etaremune(content: str) -> List[str]: """Parse multicol environment containing etaremune.""" # Remove multicol wrapper @@ -219,6 +256,28 @@ def parse_multicol_etaremune(content: str) -> List[str]: return parse_etaremune(content) +def parse_labeled_lists(content: str) -> List[tuple]: + """Parse content with labeled lists like: + \\textit{Label}: + \\begin{etaremune}...\\end{etaremune} + + Returns list of (label, items) tuples. + """ + labeled_lists = [] + + # Pattern to find labeled lists: \textit{Label}: followed by etaremune + # Also handle multicols wrapper + pattern = r'\\textit\{([^}]+)\}:\s*(?:\\begin\{multicols\}\{\d+\})?\s*\\begin\{etaremune\}(.+?)\\end\{etaremune\}\s*(?:\\end\{multicols\})?' + + for match in re.finditer(pattern, content, re.DOTALL): + label = match.group(1).strip() + list_content = match.group(2) + items = parse_single_etaremune(list_content) + labeled_lists.append((label, items)) + + return labeled_lists + + def extract_header_info(body: str) -> Dict[str, str]: """Extract header information (name, title, contact).""" info = {} @@ -254,32 +313,57 @@ def extract_header_info(body: str) -> Dict[str, str]: else: rest_of_header = header - # Split by line breaks - parts = re.split(r'\\\\(?:\[[\d.]+cm\])?', rest_of_header) - + # Split by line breaks, but track which ones have spacing + # Use a pattern that captures the spacing indicator lines = [] - for part in parts: - # Remove LaTeX comments - part = re.sub(r'%.*$', '', part, flags=re.MULTILINE) - part = part.strip() - - # Skip empty parts and stray braces - if part and part not in ['}', '{', '']: - converted = convert_latex_formatting(part) - converted = converted.strip() - # Skip empty results or just punctuation + # Find all parts and their spacing + parts_with_spacing = re.split(r'(\\\\(?:\[[\d.]+cm\])?)', rest_of_header) + + current_text = '' + for i, part in enumerate(parts_with_spacing): + if re.match(r'\\\\(?:\[[\d.]+cm\])?', part): + # This is a line break - check if it has spacing + has_spacing = '[' in part and 'cm]' in part + if current_text.strip(): + # Remove LaTeX comments + text = re.sub(r'%.*$', '', current_text, flags=re.MULTILINE).strip() + if text and text not in ['}', '{', '']: + converted = convert_latex_formatting(text).strip() + if converted and converted not in ['}', '{', '']: + lines.append({'text': converted, 'space_after': has_spacing}) + current_text = '' + else: + current_text += part + + # Handle last part + if current_text.strip(): + text = re.sub(r'%.*$', '', current_text, flags=re.MULTILINE).strip() + if text and text not in ['}', '{', '']: + converted = convert_latex_formatting(text).strip() if converted and converted not in ['}', '{', '']: - lines.append(converted) + lines.append({'text': converted, 'space_after': False}) info['header_lines'] = lines return info +def remove_latex_comments(text: str) -> str: + """Remove LaTeX comments from text.""" + # Remove full-line comments + text = re.sub(r'^%.*$', '', text, flags=re.MULTILINE) + # Remove inline comments (but not escaped \%) + text = re.sub(r'(? List[CVSection]: """Extract all sections from the CV.""" sections = [] + # Remove comments BEFORE splitting to avoid commented-out subsections + body = remove_latex_comments(body) + # Split by section* or section section_pattern = r'\\section\*?\{([^}]+)\}' parts = re.split(section_pattern, body) @@ -330,20 +414,99 @@ def render_list_items(items: List[str], reversed_numbering: bool = True) -> str: return html +def render_labeled_lists(labeled_lists: List[tuple], use_two_column: bool = False) -> str: + """Render labeled lists (like Mentorship section) to HTML.""" + html = '' + for label, items in labeled_lists: + html += f'

{label}

\n' + # Apply 2-column layout for Undergraduate Advisees or when explicitly requested + is_undergrad = 'undergraduate' in label.lower() + if (use_two_column or is_undergrad) and len(items) > 10: + html += f'
{render_list_items(items)}
\n' + else: + html += render_list_items(items) + return html + + +def extract_footnote(content: str) -> tuple: + """Extract blfootnote content from text. Returns (footnote_text, cleaned_content).""" + pattern = r'\\blfootnote\{' + match = re.search(pattern, content) + if not match: + return None, content + + brace_pos = match.end() - 1 + footnote_content, end_pos = balanced_braces_extract(content, brace_pos) + if footnote_content: + # Convert LaTeX formatting in footnote + footnote_html = convert_latex_formatting(footnote_content) + # Remove the footnote from content + cleaned = content[:match.start()] + content[end_pos:] + return footnote_html, cleaned + return None, content + + +def preprocess_content(content: str, extract_footnotes: bool = False) -> tuple: + """Preprocess content to handle problematic LaTeX commands before parsing. + + If extract_footnotes is True, returns (footnote, cleaned_content). + Otherwise returns just cleaned_content for backwards compatibility. + """ + # Remove comments first + content = re.sub(r'^%.*$', '', content, flags=re.MULTILINE) + content = re.sub(r'(? str: """Render section content to HTML based on section type.""" + # Extract footnote first (for display as note under section header) + footnote, content = preprocess_content(content, extract_footnotes=True) + + # Build HTML with optional footnote note + html_prefix = '' + if footnote: + html_prefix = f'

{footnote}

\n' + + # Check for labeled lists (like in Mentorship section) + labeled_lists = parse_labeled_lists(content) + if labeled_lists: + # Check if this is the undergraduate advisees (use two-column) + use_two_col = 'undergraduate' in section_title.lower() + return html_prefix + render_labeled_lists(labeled_lists, use_two_column=use_two_col) + # Check for etaremune lists if r'\begin{etaremune}' in content: if r'\begin{multicols}' in content: items = parse_multicol_etaremune(content) if 'talks' in section_title.lower() or 'undergraduate' in section_title.lower(): - return f'
{render_list_items(items)}
' + return html_prefix + f'
{render_list_items(items)}
' else: - return render_list_items(items) + return html_prefix + render_list_items(items) else: items = parse_etaremune(content) - return render_list_items(items) + # Check if should be 2 columns based on section + if 'undergraduate' in section_title.lower(): + return html_prefix + f'
{render_list_items(items)}
' + return html_prefix + render_list_items(items) # For regular content, convert formatting content = convert_latex_formatting(content) @@ -351,10 +514,13 @@ def render_section_content(content: str, section_title: str) -> str: # Split into paragraphs paragraphs = re.split(r'\n\s*\n', content) - html = '' + html = html_prefix for para in paragraphs: para = para.strip() if para: + # Skip empty-looking content or raw LaTeX remnants + if para in ['}', '{', ''] or para.startswith('\\'): + continue if not para.startswith('<'): html += f'

{para}

\n' else: @@ -382,7 +548,10 @@ def generate_html(tex_content: str) -> str:
- Download CV as PDF +
+ Curriculum Vitae + Download PDF +
@@ -394,9 +563,14 @@ def generate_html(tex_content: str) -> str: html_parts.append(f'

{header_info["name"]}

\n') if 'header_lines' in header_info: html_parts.append('
\n') - for line in header_info['header_lines']: - if line.strip(): - html_parts.append(f'

{line}

\n') + for line_info in header_info['header_lines']: + if isinstance(line_info, dict): + text = line_info['text'] + space_class = ' class="space-after"' if line_info.get('space_after') else '' + if text.strip(): + html_parts.append(f' {text}

\n') + elif line_info.strip(): # Backwards compatibility + html_parts.append(f'

{line_info}

\n') html_parts.append('
\n') html_parts.append(' \n\n') @@ -413,10 +587,13 @@ def generate_html(tex_content: str) -> str: html_parts.append(f' {rendered}\n') for subsection in section.subsections: + rendered = render_section_content(subsection.content, subsection.title) + # Skip empty subsections + if not rendered.strip(): + continue sub_id = re.sub(r'[^a-z0-9]+', '-', subsection.title.lower()).strip('-') html_parts.append(f'
\n') html_parts.append(f'

{subsection.title}

\n') - rendered = render_section_content(subsection.content, subsection.title) html_parts.append(f' {rendered}\n') html_parts.append('
\n') else: diff --git a/tests/test_build_cv.py b/tests/test_build_cv.py index 6657ac5..12b8a0b 100644 --- a/tests/test_build_cv.py +++ b/tests/test_build_cv.py @@ -260,7 +260,9 @@ def test_extract_header_lines(self): info = extract_header_info(body) assert 'header_lines' in info assert len(info['header_lines']) > 0 - assert any('Department' in line for line in info['header_lines']) + # header_lines is a list of dicts with 'text' and 'space_after' keys + texts = [line['text'] if isinstance(line, dict) else line for line in info['header_lines']] + assert any('Department' in text for text in texts) class TestExtractSections: @@ -346,7 +348,7 @@ def test_html_has_download_button(self): ''' html = generate_html(tex_content) assert 'cv-download-bar' in html - assert 'Download CV as PDF' in html + assert 'Download PDF' in html def test_html_has_css_link(self): """Test that HTML includes CSS link.""" @@ -790,7 +792,7 @@ def test_extract_cv_from_actual_file(self): # Should have download button assert 'cv-download-bar' in content - assert 'Download CV as PDF' in content + assert 'Download PDF' in content class TestEdgeCases: From b72583b9bf9303b3bfede0463476fdf0a47a112a Mon Sep 17 00:00:00 2001 From: Jeremy Manning Date: Sun, 14 Dec 2025 15:15:25 -0500 Subject: [PATCH 4/4] Update lab manual link to HTML version MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Change link from GitHub PDF to https://context-lab.com/lab-manual/ 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- research.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/research.html b/research.html index f0a9ef1..b52690e 100644 --- a/research.html +++ b/research.html @@ -57,7 +57,7 @@