fix(latex): brace bracket-leading bullets, document escapes, pin pdftotext encoding

Three findings from the 2026-08-19 review (F9, F31, F34):

- F9: every placeholder bullet written as \item [text] let LaTeX parse
  the bracketed text as the item's optional label, rendering it clipped
  off the left page edge and absent from the PDF text layer ("Achievement"
  appeared 9 times in cv/main_example.tex and 0 times in the extraction,
  with a clean compile and green CI). Bullets are now braced as
  \item {[text]} in the example CV and in the template
  06-cover-letter-templates.md teaches, and CI's stock PDF assertions
  additionally require "Achievement" to survive pdftotext.

- F31: 05-cv-templates.md gains a "LaTeX Special Characters" section and
  06's is completed beyond \_ and \&. The load-bearing case is an
  unescaped % in a quantified achievement bullet: it starts a LaTeX
  comment and silently deletes the rest of the line from the PDF.

- F34: the documented ATS extraction commands (apply.md,
  05-cv-templates.md, CLAUDE.md) now carry -enc UTF-8. Xpdf-based
  pdftotext builds default to Latin-1 output, so a correct non-ASCII CV
  failed the replacement-character parseability check.

framework_version: 05-cv-templates.md 1.4.1 -> 1.4.2,
06-cover-letter-templates.md 1.0.1 -> 1.0.2. All three pinned by the new
tests/test_latex_guidance.py (9 tests; suite now 261).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Mads Lorentzen
2026-08-19 19:44:36 +02:00
co-authored by Claude Opus 5
parent 40022dd8b9
commit b2545d5121
8 changed files with 217 additions and 24 deletions
+2 -2
View File
@@ -254,12 +254,12 @@ Do not proceed to Step 6 until both PDFs pass inspection.
An ATS parser reads the PDF's embedded **text layer**, not the rendered page — a CV that passed visual inspection can still extract as garbage (icon glyphs where the contact details should be, scrambled reading order in multi-column layouts). This step verifies what a parser actually sees. It applies to the **CV only**; cover letters rarely go through keyword screening. An ATS parser reads the PDF's embedded **text layer**, not the rendered page — a CV that passed visual inspection can still extract as garbage (icon glyphs where the contact details should be, scrambled reading order in multi-column layouts). This step verifies what a parser actually sees. It applies to the **CV only**; cover letters rarely go through keyword screening.
**Availability check:** run `pdftotext -v`. `pdftotext` (poppler) is an optional dependency, not part of TeX distributions. If it is missing, print a one-line warning that the mechanical parse check is skipped, do the keyword-coverage check (item 3 below) against your visual Read of the PDF instead, and note the degraded mode in the Step 6 report. Same graceful-skip pattern as the salary lookup. **Availability check:** run `pdftotext -v`. `pdftotext` (poppler) is an optional dependency, not part of TeX distributions. If it is missing, print a one-line warning that the mechanical parse check is skipped, do the keyword-coverage check (item 3 below) against your visual Read of the PDF instead, and note the degraded mode in the Step 6 report. Same graceful-skip pattern as the salary lookup. Keep the `-enc UTF-8` flag: Xpdf-based builds default to Latin-1 output, and without it a correct non-ASCII CV fails the replacement-character check below.
**1. Extract the text layer:** **1. Extract the text layer:**
```bash ```bash
cd cv && pdftotext -layout main_<company>_<role>.pdf main_<company>_<role>.txt cd cv && pdftotext -layout -enc UTF-8 main_<company>_<role>.pdf main_<company>_<role>.txt
``` ```
Read the `.txt` file. Read the `.txt` file.
@@ -1,5 +1,5 @@
--- ---
framework_version: 1.4.1 framework_version: 1.4.2
--- ---
# CV Templates and Tailoring Guide # CV Templates and Tailoring Guide
@@ -211,6 +211,27 @@ Wherever the CV names a verifiable artifact - a public project, a hackathon entr
- End with: "More references are available upon request." - End with: "More references are available upon request."
- **Do not attach reference letters** - employers typically contact references directly - **Do not attach reference letters** - employers typically contact references directly
### LaTeX Special Characters (important)
Postings and profile data arrive as plain text; the CV is LaTeX. Escape these wherever they land in body text - company names, achievement bullets, skill lists:
| Character | Write | Typical trigger |
|---|---|---|
| `&` | `\&` | company names: Bang \& Olufsen, Brüel \& Kjær, H\&M |
| `%` | `\%` | quantified achievements: "cut latency by 40\%" |
| `$` | `\$` | salary and cost figures |
| `#` | `\#` | "ranked \#1", C\# |
| `_` | `\_` | file names, code identifiers |
| `~` | `\textasciitilde{}` | URLs, "approx. 5 years" tildes |
| `^` | `\textasciicircum{}` | version strings, math |
Two failure modes deserve special care:
- **`%` fails silently.** An unescaped `%` starts a LaTeX comment: the compile succeeds with zero errors, and everything after the `%` on that line vanishes from the PDF. `Cut inference latency by 40% and saved DKK 2M annually` renders as "Cut inference latency by 40" - the bullet keeps its impressive-looking fragment and loses the actual result. Quantified achievement bullets are exactly where the guidance steers you ("use numbers where possible"), so check every `%` in every bullet before compiling.
- **`&` fails loudly** inside `\cventry` (alignment-tab errors, `Missing } inserted`). The compile loop catches it, but escape employer names up front rather than debugging the compile.
Related trap: a bullet whose text begins with a literal `[` must be braced - `\item {[text]}` - or LaTeX parses the bracketed text as `\item`'s optional label and renders it clipped off the left page edge with a clean compile. The example CV's placeholder bullets are braced for exactly this reason.
## Compile-and-Inspect Loop (MANDATORY) ## Compile-and-Inspect Loop (MANDATORY)
After writing the CV and before presenting to the user, always compile and visually inspect the PDF. Iterate until the layout is clean. Workflow: After writing the CV and before presenting to the user, always compile and visually inspect the PDF. Iterate until the layout is clean. Workflow:
@@ -246,10 +267,10 @@ Restore the highest-relevance item that was previously cut — a CV that ends mi
Most employers run CVs through an ATS before a human sees them, and the ATS reads the PDF's embedded **text layer**, not the rendered page. A CV can pass visual inspection and still extract as garbage. After the layout passes the compile-and-inspect loop, verify the text layer: Most employers run CVs through an ATS before a human sees them, and the ATS reads the PDF's embedded **text layer**, not the rendered page. A CV can pass visual inspection and still extract as garbage. After the layout passes the compile-and-inspect loop, verify the text layer:
```bash ```bash
cd cv && pdftotext -layout main_<company>_<role>.pdf main_<company>_<role>.txt cd cv && pdftotext -layout -enc UTF-8 main_<company>_<role>.pdf main_<company>_<role>.txt
``` ```
`pdftotext` comes from [poppler](https://poppler.freedesktop.org/), not the TeX distribution - it is an **optional** dependency. If it is not installed, skip the mechanical check with a warning and rely on the visual PDF read for keyword coverage. `pdftotext` comes from [poppler](https://poppler.freedesktop.org/), not the TeX distribution - it is an **optional** dependency. The `-enc UTF-8` flag is not optional: Xpdf-based `pdftotext` builds default to Latin-1 output, which makes every non-ASCII character in a perfectly good CV read back as a replacement character and fail the parseability check below for no real reason. If it is not installed, skip the mechanical check with a warning and rely on the visual PDF read for keyword coverage.
What to check in the extraction: What to check in the extraction:
@@ -1,5 +1,5 @@
--- ---
framework_version: 1.0.1 framework_version: 1.0.2
--- ---
# Cover Letter Templates and Tailoring Guide # Cover Letter Templates and Tailoring Guide
@@ -92,9 +92,9 @@ The font wrapper is mandatory — if you just move `\begin{itemize}` outside `\l
{\raggedright\fontspec[Path = OpenFonts/fonts/raleway/]{Raleway-Medium}\fontsize{11pt}{13pt}\selectfont {\raggedright\fontspec[Path = OpenFonts/fonts/raleway/]{Raleway-Medium}\fontsize{11pt}{13pt}\selectfont
\begin{itemize} \begin{itemize}
\item [Concrete achievement/skill 1] \item {[Concrete achievement/skill 1]}
\item [Concrete achievement/skill 2] \item {[Concrete achievement/skill 2]}
\item [Concrete achievement/skill 3] \item {[Concrete achievement/skill 3]}
\end{itemize}\par} \end{itemize}\par}
\lettercontent{[Connection to company - why this role, why this company specifically]} \lettercontent{[Connection to company - why this role, why this company specifically]}
@@ -146,10 +146,14 @@ The font wrapper is mandatory — if you just move `\begin{itemize}` outside `\l
- 3-5 bullets is ideal - 3-5 bullets is ideal
- Start each bullet with bold label or action verb - Start each bullet with bold label or action verb
- Use `\textbf{Label:}` for category-style bullets - Use `\textbf{Label:}` for category-style bullets
- A bullet whose text begins with a literal `[` must be braced: `\item {[text]}`. Unbraced, LaTeX parses `[text]` as `\item`'s optional label and renders it off the left page edge, missing from the PDF text layer entirely
### LaTeX Special Characters ### LaTeX Special Characters
- Underscore: `\_` Escape these wherever they appear in body text:
- Ampersand: `\&` - Ampersand: `\&` (company names: Brüel \& Kjær, H\&M) - unescaped, the compile fails loudly
- Percent: `\%` ("grew revenue 30\%") - unescaped, it does **not** fail: everything after the `%` on that line is silently eaten as a LaTeX comment
- Dollar: `\$`, hash: `\#`, underscore: `\_`
- Tilde: `\textasciitilde{}`, caret: `\textasciicircum{}`, backslash: `\textbackslash{}`
### Non-English Cover Letters ### Non-English Cover Letters
- Same template structure, just write content in the posting's language - Same template structure, just write content in the posting's language
+2 -1
View File
@@ -144,7 +144,8 @@ jobs:
python3 tools/verify_pdf.py cv/main_example.pdf \ python3 tools/verify_pdf.py cv/main_example.pdf \
--pages 2 \ --pages 2 \
--contains '[your.email@example.com]' \ --contains '[your.email@example.com]' \
--contains 'Professional Experience' --contains 'Professional Experience' \
--contains 'Achievement'
python3 tools/verify_pdf.py cover_letters/cover_example.pdf \ python3 tools/verify_pdf.py cover_letters/cover_example.pdf \
--pages 1 \ --pages 1 \
--contains 'your.email@example.com' \ --contains 'your.email@example.com' \
+27
View File
@@ -15,6 +15,16 @@ per-file diff commands.
### Added ### Added
- **LaTeX special-character guidance for CVs** (`framework_version` 1.4.1 -> 1.4.2 in
`05-cv-templates.md`, 1.0.1 -> 1.0.2 in `06-cover-letter-templates.md`) - `05` gains a
"LaTeX Special Characters" section and `06`'s existing one is completed beyond `\_`/`\&`.
The load-bearing case is an unescaped `%` in a quantified achievement bullet: it starts a
LaTeX comment, so "cut latency by 40% and saved DKK 2M" compiles with zero errors and
renders as "cut latency by 40" - silent content loss in the deliverable, on exactly the
content the guidance steers users to write. `&` in employer names (Bang & Olufsen, H&M)
fails loudly at compile time and is now documented alongside. Pinned by
`tests/test_latex_guidance.py`.
- **`seen_jobs.json` entries record which mechanism produced them** - a new additive `source` - **`seen_jobs.json` entries record which mechanism produced them** - a new additive `source`
field (`cli` for Step 1b portal-CLI output, `websearch` for the Step 1c fallback), a Step 1c field (`cli` for Step 1b portal-CLI output, `websearch` for the Step 1c fallback), a Step 1c
rule tagging fallback results at collection time, and a `fallback (websearch):` line in the rule tagging fallback results at collection time, and a `fallback (websearch):` line in the
@@ -46,6 +56,23 @@ per-file diff commands.
### Fixed ### Fixed
- **Example-CV bullets no longer swallowed as LaTeX optional labels** - every placeholder
bullet written as `\item [text]` (11 in `cv/main_example.tex`, 3 in
`06-cover-letter-templates.md`'s taught template) let LaTeX parse the bracketed text as
`\item`'s optional argument: the shipped example CV rendered all Professional Experience
bullets clipped off the left page edge, with the word "Achievement" appearing 9 times in
the source and 0 times in the PDF text layer - a clean compile, green CI. Bullets are now
braced (`\item {[text]}`), the cover-letter guide teaches the braced form, and CI's stock
PDF assertions additionally require `Achievement` to survive `pdftotext`. Pinned by
`tests/test_latex_guidance.py`.
- **Documented ATS extraction commands pin `-enc UTF-8`** - `pdftotext -layout` without an
encoding flag emits Latin-1 on Xpdf builds, so every non-ASCII character in a correct CV
(Rambøll, Ingeniør, København) read back as a replacement character and failed the
parseability checklist, steering the agent to "fix" a healthy document. The commands in
`apply.md`, `05-cv-templates.md`, and `CLAUDE.md`'s verification checklist now carry
`-enc UTF-8`, which is deterministic on both poppler and Xpdf. Pinned by
`tests/test_latex_guidance.py`.
- **`jobbank-search` search output now carries the `/scrape` contract's `date` field** (#342) - - **`jobbank-search` search output now carries the `/scrape` contract's `date` field** (#342) -
the CLI emitted `posted` (full ISO 8601) but not the cross-portal `date` key, the one Step 2 the CLI emitted `posted` (full ISO 8601) but not the cross-portal `date` key, the one Step 2
contract field it was missing. Search results now additively emit `date` as `YYYY-MM-DD` contract field it was missing. Search results now additively emit `date` as `YYYY-MM-DD`
+1 -1
View File
@@ -140,7 +140,7 @@ Both documents MUST be compiled and visually inspected via the Read tool on the
- [ ] **Cover letter bullet font matches body font** - `\lettercontent{}` must not wrap `\begin{itemize}...\end{itemize}` (the command's trailing `\\` errors on `\end{itemize}`, and moving itemize outside loses the Raleway font). Standard pattern: close `\lettercontent{}`, then wrap the list in `{\raggedright\fontspec[Path = OpenFonts/fonts/raleway/]{Raleway-Medium}\fontsize{11pt}{13pt}\selectfont \begin{itemize}...\end{itemize}\par}` - [ ] **Cover letter bullet font matches body font** - `\lettercontent{}` must not wrap `\begin{itemize}...\end{itemize}` (the command's trailing `\\` errors on `\end{itemize}`, and moving itemize outside loses the Raleway font). Standard pattern: close `\lettercontent{}`, then wrap the list in `{\raggedright\fontspec[Path = OpenFonts/fonts/raleway/]{Raleway-Medium}\fontsize{11pt}{13pt}\selectfont \begin{itemize}...\end{itemize}\par}`
### ATS & keyword verification (CV) ### ATS & keyword verification (CV)
ATS parsers read the PDF's embedded text layer, not the rendered page. Extract it with `pdftotext -layout` and verify what a parser sees. `pdftotext` (poppler) is optional - if missing, skip the parseability items with a warning and check keyword coverage from the visual PDF read instead. ATS parsers read the PDF's embedded text layer, not the rendered page. Extract it with `pdftotext -layout -enc UTF-8` and verify what a parser sees. `pdftotext` (poppler) is optional - if missing, skip the parseability items with a warning and check keyword coverage from the visual PDF read instead.
- [ ] CV text layer extracts cleanly - no `(cid:*)` markers, `` replacement characters, or text visible in the PDF but absent from the extraction - [ ] CV text layer extracts cleanly - no `(cid:*)` markers, `` replacement characters, or text visible in the PDF but absent from the extraction
- [ ] Email and phone appear as **literal text** in the extraction (icon-glyph noise like `MOBILE-ALT`/`Envelope` is harmless, but a contact detail carried only by an icon or hyperlink is invisible to ATS) - [ ] Email and phone appear as **literal text** in the extraction (icon-glyph noise like `MOBILE-ALT`/`Envelope` is harmless, but a contact detail carried only by an icon or hyperlink is invisible to ATS)
- [ ] Reading order of the extracted text matches the visual order (single-column stock template is safe; multi-column custom templates are where this breaks) - [ ] Reading order of the extracted text matches the visual order (single-column stock template is safe; multi-column custom templates are where this breaks)
+11 -11
View File
@@ -93,10 +93,10 @@
% --- Most Recent Role --- % --- Most Recent Role ---
\item{\cventry{[YYYY-Present]}{[Job Title]}{[Company]}{[City, Country]}{}{\vspace{1pt} \item{\cventry{[YYYY-Present]}{[Job Title]}{[Company]}{[City, Country]}{}{\vspace{1pt}
\begin{itemize} \begin{itemize}
\item [Achievement or responsibility 1 - be specific, use numbers where possible] \item {[Achievement or responsibility 1 - be specific, use numbers where possible]}
\item [Achievement or responsibility 2] \item {[Achievement or responsibility 2]}
\item [Achievement or responsibility 3] \item {[Achievement or responsibility 3]}
\item [Achievement or responsibility 4] \item {[Achievement or responsibility 4]}
\end{itemize}}} \end{itemize}}}
\vspace{3pt} \vspace{3pt}
@@ -104,9 +104,9 @@
% --- Previous Role --- % --- Previous Role ---
\item{\cventry{[YYYY-YYYY]}{[Job Title]}{[Company]}{[City, Country]}{}{\vspace{1pt} \item{\cventry{[YYYY-YYYY]}{[Job Title]}{[Company]}{[City, Country]}{}{\vspace{1pt}
\begin{itemize} \begin{itemize}
\item [Achievement or responsibility 1] \item {[Achievement or responsibility 1]}
\item [Achievement or responsibility 2] \item {[Achievement or responsibility 2]}
\item [Achievement or responsibility 3] \item {[Achievement or responsibility 3]}
\end{itemize}}} \end{itemize}}}
\vspace{3pt} \vspace{3pt}
@@ -114,8 +114,8 @@
% --- Earlier Role --- % --- Earlier Role ---
\item{\cventry{[YYYY-YYYY]}{[Job Title]}{[Company]}{[City, Country]}{}{\vspace{1pt} \item{\cventry{[YYYY-YYYY]}{[Job Title]}{[Company]}{[City, Country]}{}{\vspace{1pt}
\begin{itemize} \begin{itemize}
\item [Achievement or responsibility 1] \item {[Achievement or responsibility 1]}
\item [Achievement or responsibility 2] \item {[Achievement or responsibility 2]}
\end{itemize}}} \end{itemize}}}
\end{itemize} \end{itemize}
@@ -147,7 +147,7 @@ Thesis: ``[Thesis Title].'' [Brief description of research focus.]
\section{Languages} \section{Languages}
\vspace{1pt} \vspace{1pt}
\begin{itemize} \begin{itemize}
\item [Language 1] (native), [Language 2] (fluent), [Language 3] (intermediate). \item {[Language 1] (native), [Language 2] (fluent), [Language 3] (intermediate).}
\end{itemize} \end{itemize}
% ============================================================ % ============================================================
@@ -157,7 +157,7 @@ Thesis: ``[Thesis Title].'' [Brief description of research focus.]
\section{Publications} \section{Publications}
\vspace{1pt} \vspace{1pt}
\begin{itemize} \begin{itemize}
\item [Author(s)] ([Year]). [Title]. [Journal/Conference]. \href{[DOI_URL]}{DOI link} \item {[Author(s)] ([Year]). [Title]. [Journal/Conference]. \href{[DOI_URL]}{DOI link}}
\end{itemize} \end{itemize}
% ============================================================ % ============================================================
+140
View File
@@ -0,0 +1,140 @@
"""Guards for the LaTeX authoring guidance and the example documents.
Three silent-failure modes live here, all found by the 2026-08-19 review
(F9, F31, F34). Each one produces a clean compile and a green CI run
while the rendered document or its ATS extraction is wrong, so the spec
files and the example sources are the only place a test can catch them:
- F9: a bullet written as `\\item [text]` is parsed as moderncv's
optional label, rendered off the left page edge, and dropped from the
PDF text layer. The example CV shipped that way for months.
- F31: an unescaped `%` in body text silently truncates the rest of the
line (`&` at least fails loudly). The guidance must name the escapes.
- F34: `pdftotext` without `-enc UTF-8` emits Latin-1 on Xpdf builds,
so a correct Danish CV fails the documented "no replacement
characters" check and the agent is sent to "fix" a healthy document.
"""
import re
import unittest
from pathlib import Path
REPO = Path(__file__).resolve().parent.parent
SKILL_DIR = REPO / ".claude" / "skills" / "job-application-assistant"
CV_TEMPLATES = SKILL_DIR / "05-cv-templates.md"
COVER_TEMPLATES = SKILL_DIR / "06-cover-letter-templates.md"
APPLY = REPO / ".claude" / "commands" / "apply.md"
EXAMPLE_CV = REPO / "cv" / "main_example.tex"
EXAMPLE_COVER = REPO / "cover_letters" / "cover_example.tex"
# \item whose body starts with [ - with or without whitespace between.
# LaTeX skips spaces while scanning for the optional argument, so
# `\item [text]` and `\item[text]` both swallow the text as a label.
# The safe spelling `\item {[text]}` does not match.
UNBRACED_BRACKET_ITEM = re.compile(r"\\item\s*\[")
# The escapes both guidance files must document. `%` is the load-bearing
# one: it truncates silently. The others fail loudly or corrupt spacing.
REQUIRED_ESCAPES = ["\\&", "\\%", "\\$", "\\#", "\\_"]
def section(text, heading):
"""Return the body of a markdown section up to the next heading."""
pattern = re.compile(
rf"^#+ {re.escape(heading)}[^\n]*\n(.*?)(?=^#+ |\Z)",
re.MULTILINE | re.DOTALL,
)
match = pattern.search(text)
return match.group(1) if match else None
class TestBulletBracketTrap(unittest.TestCase):
"""F9: no document or template doc may teach `\\item [text]`."""
def assert_no_unbraced_bracket_items(self, path):
offending = [
f"{path.name}:{lineno}: {line.strip()}"
for lineno, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1)
if UNBRACED_BRACKET_ITEM.search(line)
]
self.assertEqual(
offending,
[],
"\\item followed by [ is parsed as an optional label and the "
"text is clipped off the page; write \\item {[...]} instead:\n"
+ "\n".join(offending),
)
def test_example_cv_has_no_bracket_labelled_bullets(self):
self.assert_no_unbraced_bracket_items(EXAMPLE_CV)
def test_example_cover_letter_has_no_bracket_labelled_bullets(self):
self.assert_no_unbraced_bracket_items(EXAMPLE_COVER)
def test_cover_letter_guide_does_not_teach_the_broken_pattern(self):
self.assert_no_unbraced_bracket_items(COVER_TEMPLATES)
def test_cv_guide_does_not_teach_the_broken_pattern(self):
self.assert_no_unbraced_bracket_items(CV_TEMPLATES)
class TestSpecialCharacterGuidance(unittest.TestCase):
"""F31: both template guides must document the LaTeX escapes."""
def assert_escapes_documented(self, path):
body = section(path.read_text(encoding="utf-8"), "LaTeX Special Characters")
self.assertIsNotNone(
body, f"{path.name} has no 'LaTeX Special Characters' section"
)
missing = [esc for esc in REQUIRED_ESCAPES if esc not in body]
self.assertEqual(
missing,
[],
f"{path.name}'s special-characters section is missing: {missing}",
)
def test_cv_guide_documents_the_escapes(self):
self.assert_escapes_documented(CV_TEMPLATES)
def test_cover_letter_guide_documents_the_escapes(self):
self.assert_escapes_documented(COVER_TEMPLATES)
def test_cv_guide_warns_that_percent_truncates_silently(self):
body = section(
CV_TEMPLATES.read_text(encoding="utf-8"), "LaTeX Special Characters"
)
self.assertIsNotNone(body)
self.assertRegex(
body,
re.compile(r"silent", re.IGNORECASE),
"the % failure mode must be called out as silent - it is the "
"reason this section exists (a clean compile with the rest of "
"the bullet gone)",
)
class TestAtsExtractionEncoding(unittest.TestCase):
"""F34: every documented extraction command must pin the encoding."""
def assert_pdftotext_commands_pin_utf8(self, path):
offending = [
f"{path.name}:{lineno}: {line.strip()}"
for lineno, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1)
if "pdftotext" in line and "-layout" in line and "-enc UTF-8" not in line
]
self.assertEqual(
offending,
[],
"pdftotext without -enc UTF-8 emits Latin-1 on Xpdf builds, so "
"the ATS check reports phantom replacement characters on any "
"non-ASCII CV; add -enc UTF-8:\n" + "\n".join(offending),
)
def test_apply_extraction_command_pins_utf8(self):
self.assert_pdftotext_commands_pin_utf8(APPLY)
def test_cv_guide_extraction_command_pins_utf8(self):
self.assert_pdftotext_commands_pin_utf8(CV_TEMPLATES)
if __name__ == "__main__":
unittest.main()