resumectl 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/GLOSSARY.md +19 -0
- package/LICENSE +21 -0
- package/README.md +63 -2
- package/docs/adr/0001-domain-first-organization.md +32 -0
- package/docs/agents/domain.md +12 -0
- package/docs/agents/issue-tracker.md +10 -0
- package/docs/agents/triage-labels.md +11 -0
- package/docs/api.md +16 -0
- package/docs/configuration.md +83 -0
- package/docs/quality.md +36 -0
- package/docs/rendering.md +68 -0
- package/docs/sans.png +0 -0
- package/docs/serif.png +0 -0
- package/docs/setup.md +31 -0
- package/package.json +48 -4
- package/resume.config.schema.json +85 -0
- package/resume.config.yaml +65 -0
- package/skills/resumectl/SKILL.md +143 -0
- package/src/cli/main.ts +108 -0
- package/src/cli.ts +14 -0
- package/src/content/tex.ts +107 -0
- package/src/errors.ts +3 -0
- package/src/index.ts +22 -0
- package/src/project/config.ts +265 -0
- package/src/project/doctor.ts +76 -0
- package/src/project/init.ts +78 -0
- package/src/quality/gates.ts +113 -0
- package/src/quality/pdf.ts +84 -0
- package/src/rendering/assets.ts +4 -0
- package/src/rendering/compile.ts +101 -0
- package/src/rendering/font-metrics.ts +89 -0
- package/src/rendering/font-width.ts +11 -0
- package/src/rendering/measurements.ts +81 -0
- package/src/rendering/metrics/sans.json +122 -0
- package/src/rendering/metrics/serif.json +121 -0
- package/template/fonts/Inter-430.ttf +0 -0
- package/template/fonts/Inter-500.ttf +0 -0
- package/template/fonts/Inter-600.ttf +0 -0
- package/template/fonts/OFL-Inter.txt +93 -0
- package/template/resume-measure.tex +74 -0
- package/template/resume.cls +203 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
% Loaded by resume.cls only for CLI builds. Zero-width position records are
|
|
2
|
+
% written at shipout; paragraph line counts come from TeX's actual line breaker.
|
|
3
|
+
% Paragraph hooks: https://www.latex-project.org/help/documentation/ltpara-doc.pdf
|
|
4
|
+
\newwrite\resume@measurestream
|
|
5
|
+
\immediate\openout\resume@measurestream=\jobname.rmlayout
|
|
6
|
+
\newcount\resume@itemid
|
|
7
|
+
\newcount\resume@paragraphid
|
|
8
|
+
\newif\ifresume@entry
|
|
9
|
+
\def\resume@beginentry{%
|
|
10
|
+
\global\advance\resume@itemid by1
|
|
11
|
+
\global\resume@paragraphid=0
|
|
12
|
+
\immediate\write\resume@measurestream{B|\the\resume@itemid|\expandafter\detokenize\expandafter{\resume@sectiontitle}}%
|
|
13
|
+
}
|
|
14
|
+
\RenewDocumentCommand\resumeentry{+m}{%
|
|
15
|
+
\par
|
|
16
|
+
\begingroup
|
|
17
|
+
\ifresume@entry\PackageError{resume}{Do not nest resumeentry}{Use one resumeentry per paragraph or table description.}\fi
|
|
18
|
+
\ifnum\@listdepth>0\PackageError{resume}{Do not use resumeentry inside a list}{List items are measured automatically.}\fi
|
|
19
|
+
\resume@entrytrue\resume@beginentry
|
|
20
|
+
#1\par
|
|
21
|
+
\endgroup
|
|
22
|
+
}
|
|
23
|
+
\def\resume@ifmeasuring#1{%
|
|
24
|
+
\ifresume@entry #1\else
|
|
25
|
+
\ifnum\@itemdepth=1\ifnum\@listdepth=1 #1\fi\fi
|
|
26
|
+
\fi
|
|
27
|
+
}
|
|
28
|
+
\def\resume@sectiontitle{}
|
|
29
|
+
\NewCommandCopy\resume@originalsection\section
|
|
30
|
+
\RenewDocumentCommand\section{s o m}{%
|
|
31
|
+
\par
|
|
32
|
+
\gdef\resume@sectiontitle{#3}%
|
|
33
|
+
\IfBooleanTF{#1}{\resume@originalsection*{#3}}{%
|
|
34
|
+
\IfNoValueTF{#2}{\resume@originalsection{#3}}{\resume@originalsection[#2]{#3}}}%
|
|
35
|
+
}
|
|
36
|
+
\NewCommandCopy\resume@originalitem\item
|
|
37
|
+
\RenewDocumentCommand\item{o}{%
|
|
38
|
+
\IfNoValueTF{#1}{\resume@originalitem}{\resume@originalitem[#1]}%
|
|
39
|
+
\ifnum\@itemdepth=1\ifnum\@listdepth=1
|
|
40
|
+
\resume@beginentry
|
|
41
|
+
\fi\fi
|
|
42
|
+
}
|
|
43
|
+
\def\resume@position#1{%
|
|
44
|
+
\pdfsavepos
|
|
45
|
+
% Freeze the IDs now, but expand positions only when this node ships out.
|
|
46
|
+
\edef\resume@writeposition{\noexpand\write\resume@measurestream{#1|\the\resume@itemid|\the\resume@paragraphid|\noexpand\the\noexpand\pdflastxpos|\noexpand\the\noexpand\pdflastypos}}%
|
|
47
|
+
\resume@writeposition
|
|
48
|
+
}
|
|
49
|
+
\AddToHook{para/begin}{%
|
|
50
|
+
\resume@ifmeasuring{%
|
|
51
|
+
\global\advance\resume@paragraphid by1
|
|
52
|
+
\immediate\write\resume@measurestream{P|\the\resume@itemid|\the\resume@paragraphid|\number\linewidth}%
|
|
53
|
+
\resume@position{S}%
|
|
54
|
+
}%
|
|
55
|
+
}
|
|
56
|
+
\AddToHook{para/end}{%
|
|
57
|
+
\resume@ifmeasuring{\resume@position{E}}%
|
|
58
|
+
}
|
|
59
|
+
\AddToHook{para/after}{%
|
|
60
|
+
\resume@ifmeasuring{%
|
|
61
|
+
% \prevgraf must be read outside \write's expansion context (where it is 0).
|
|
62
|
+
\edef\resume@lines{\the\prevgraf}%
|
|
63
|
+
\immediate\write\resume@measurestream{L|\the\resume@itemid|\the\resume@paragraphid|\resume@lines}%
|
|
64
|
+
}%
|
|
65
|
+
}
|
|
66
|
+
% Nested lists aren't flat resume bullets; fail clearly rather than miscount them.
|
|
67
|
+
\def\resume@nestedlist{%
|
|
68
|
+
\ifresume@entry\PackageError{resume}{Do not nest lists inside resumeentry}{Use a flat list without resumeentry.}\fi
|
|
69
|
+
\ifnum\@itemdepth>0\immediate\write\resume@measurestream{U|\the\resume@itemid}\fi
|
|
70
|
+
}
|
|
71
|
+
\AddToHook{env/itemize/begin}{\resume@nestedlist}
|
|
72
|
+
\AddToHook{env/enumerate/begin}{\resume@nestedlist}
|
|
73
|
+
% enumitem lowers its depth before its final \par; finish while still in scope.
|
|
74
|
+
\AddToHook{env/itemize/end}{\par}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
% resume.cls — a one-page résumé class with a serif and a sans-serif style.
|
|
2
|
+
% Compile with XeLaTeX. MIT License.
|
|
3
|
+
%
|
|
4
|
+
% \documentclass{resume} % serif (TeX Gyre Termes)
|
|
5
|
+
% \documentclass[style=sans]{resume} % sans (Inter), upright only
|
|
6
|
+
% \documentclass[style=sans, paper=letter, scale=0.9, tracking=-1]{resume}
|
|
7
|
+
%
|
|
8
|
+
% Options (key=value):
|
|
9
|
+
% style = serif | sans default serif
|
|
10
|
+
% paper = a4 | letter default a4
|
|
11
|
+
% scale = <number> sans only: font scale vs. the 11pt body, default 0.86
|
|
12
|
+
% tracking = <number> sans only: letter spacing in % of the em, default -1.5
|
|
13
|
+
% ratio = <number> sans only: type-scale ratio between levels, default 1.125
|
|
14
|
+
%
|
|
15
|
+
% CLI builds apply resolved config after class options and measure actual bullets.
|
|
16
|
+
\NeedsTeXFormat{LaTeX2e}
|
|
17
|
+
\ProvidesClass{resume}[2026/10/03 v1.0 One-page resume, serif and sans styles]
|
|
18
|
+
|
|
19
|
+
\RequirePackage{kvoptions}
|
|
20
|
+
\SetupKeyvalOptions{family=resume, prefix=resume@}
|
|
21
|
+
\DeclareStringOption[serif]{style}
|
|
22
|
+
\DeclareStringOption[a4]{paper}
|
|
23
|
+
\DeclareStringOption[0.86]{scale}
|
|
24
|
+
\DeclareStringOption[-1.5]{tracking}
|
|
25
|
+
\DeclareStringOption[1.125]{ratio}
|
|
26
|
+
\DeclareStringOption[0.4in]{marginTop}
|
|
27
|
+
\DeclareStringOption[0.3in]{marginBottom}
|
|
28
|
+
\DeclareStringOption[0.35in]{marginLeft}
|
|
29
|
+
\DeclareStringOption[0.35in]{marginRight}
|
|
30
|
+
\DeclareStringOption[0.15em]{bulletSpacing}
|
|
31
|
+
\DeclareStringOption[1.5pc]{bulletIndent}
|
|
32
|
+
\DeclareStringOption[]{sectionBefore}
|
|
33
|
+
\DeclareStringOption[]{sectionAfter}
|
|
34
|
+
\ProcessKeyvalOptions*
|
|
35
|
+
\ifdefined\ResumeConfigOptions
|
|
36
|
+
\edef\resume@applyconfig{\noexpand\setkeys{resume}{\ResumeConfigOptions}}%
|
|
37
|
+
\resume@applyconfig
|
|
38
|
+
\fi
|
|
39
|
+
|
|
40
|
+
\LoadClass[11pt]{article}
|
|
41
|
+
|
|
42
|
+
\setlength{\parindent}{0pt}
|
|
43
|
+
\pagenumbering{gobble}
|
|
44
|
+
|
|
45
|
+
\RequirePackage[colorlinks=false, hidelinks]{hyperref}
|
|
46
|
+
\RequirePackage{url}
|
|
47
|
+
\urlstyle{same}
|
|
48
|
+
\RequirePackage{tikz}
|
|
49
|
+
\RequirePackage{tabularx}
|
|
50
|
+
|
|
51
|
+
% ---------------------------------------------------------------- fonts
|
|
52
|
+
\RequirePackage{fontspec}
|
|
53
|
+
% Tectonic ships a 2021 LaTeX format. Backport what newer formats provide:
|
|
54
|
+
% \fpeval (kernel since 2022-06), and array's \@finalstrut, which backs up a
|
|
55
|
+
% line when a p-cell ends in vertical mode instead of adding an empty one.
|
|
56
|
+
\IfFormatAtLeastTF{2022-06-01}{}{%
|
|
57
|
+
\RequirePackage{xfp}%
|
|
58
|
+
\def\@finalstrut#1{%
|
|
59
|
+
\unskip
|
|
60
|
+
\ifhmode\nobreak\else\ifnum\lastnodetype>\m@ne\vskip-\baselineskip\fi\fi
|
|
61
|
+
\vrule\@width\z@\@height\z@\@depth\dp#1}%
|
|
62
|
+
}
|
|
63
|
+
\def\resume@sans{sans}
|
|
64
|
+
\newif\ifresume@sansstyle
|
|
65
|
+
\ifx\resume@style\resume@sans\resume@sansstyletrue\fi
|
|
66
|
+
\ifx\resume@sectionBefore\@empty
|
|
67
|
+
\ifresume@sansstyle\def\resume@sectionBefore{*2}\else\def\resume@sectionBefore{*1}\fi
|
|
68
|
+
\fi
|
|
69
|
+
\ifx\resume@sectionAfter\@empty
|
|
70
|
+
\ifresume@sansstyle\def\resume@sectionAfter{*0.8}\else\def\resume@sectionAfter{*1}\fi
|
|
71
|
+
\fi
|
|
72
|
+
\ifresume@sansstyle
|
|
73
|
+
% Inter at text optical size, cut into static instances because XeTeX
|
|
74
|
+
% ignores variable-font axes. Sans style is upright only: \textit and
|
|
75
|
+
% \emph render in the body face. Weights: body 430 (offsets the thinner
|
|
76
|
+
% strokes from Scale=0.86), bold 600, name 500.
|
|
77
|
+
% Font directory: \ResumeFontDir if defined (`resumectl build` passes an
|
|
78
|
+
% absolute path), else ./fonts/ when compiling next to the class.
|
|
79
|
+
\ifdefined\ResumeFontDir
|
|
80
|
+
\defaultfontfeatures[Inter]{Path=\ResumeFontDir}
|
|
81
|
+
\else
|
|
82
|
+
\defaultfontfeatures[Inter]{Path=fonts/}
|
|
83
|
+
\fi
|
|
84
|
+
\setmainfont{Inter}[
|
|
85
|
+
Extension=.ttf,
|
|
86
|
+
Scale=\resume@scale,
|
|
87
|
+
LetterSpace=\resume@tracking,
|
|
88
|
+
% calt would swap ( ) - + next to capitals and digits for case-sensitive
|
|
89
|
+
% glyphs that have no Unicode mapping: they copy out of the PDF as
|
|
90
|
+
% private-use characters and break ATS parsing of e.g. phone numbers.
|
|
91
|
+
Contextuals=NoAlternate,
|
|
92
|
+
UprightFont=*-430,
|
|
93
|
+
BoldFont=*-600,
|
|
94
|
+
ItalicFont=*-430,
|
|
95
|
+
BoldItalicFont=*-600,
|
|
96
|
+
]
|
|
97
|
+
% Type scale: size(n) = 10.95pt x ratio^n, rendered at x scale, so n=0 is the body.
|
|
98
|
+
% n=+5 name · n=+1 organisation · n=0 body, role, dates, contact · n=-1 section label
|
|
99
|
+
% Stroke darkness (size x stem width) then falls monotonically down the levels:
|
|
100
|
+
% name 500 > organisation 600 > role 600 > section label 600 > body 430.
|
|
101
|
+
\newcommand{\typestep}[1]{%
|
|
102
|
+
\fontsize{\fpeval{round(10.95*(\resume@ratio)^(#1),2)}}%
|
|
103
|
+
{\fpeval{round(10.95*(\resume@ratio)^(#1)*1.2,2)}}\selectfont}
|
|
104
|
+
% Large sizes read heavier, so the name drops to 500; all caps get +2% tracking.
|
|
105
|
+
\newfontfamily\resume@namefont{Inter}[Extension=.ttf, Scale=\resume@scale, LetterSpace=2,
|
|
106
|
+
Contextuals=NoAlternate, UprightFont=*-500, BoldFont=*-500]
|
|
107
|
+
% Small all-caps labels need open tracking (+8%) and SemiBold to hold their level.
|
|
108
|
+
\newfontfamily\resume@labelfont{Inter}[Extension=.ttf, Scale=\resume@scale, LetterSpace=8,
|
|
109
|
+
Contextuals=NoAlternate, UprightFont=*-600, BoldFont=*-600]
|
|
110
|
+
% Tabular figures so the right-hand date column lines up digit for digit.
|
|
111
|
+
\newcommand{\resume@dates}{\normalsize\addfontfeatures{Numbers=Monospaced}}
|
|
112
|
+
% Inter carries U+2197; the arrow sits in the body weight next to a bold name.
|
|
113
|
+
\newcommand{\resume@arrow}{\textmd{↗}}
|
|
114
|
+
\else
|
|
115
|
+
% Resolve files from the active TeX distribution, so a host-installed font
|
|
116
|
+
% with the same family name cannot silently supply the serif face.
|
|
117
|
+
\setmainfont{texgyretermes}[
|
|
118
|
+
Extension=.otf,
|
|
119
|
+
UprightFont=*-regular,
|
|
120
|
+
BoldFont=*-bold,
|
|
121
|
+
ItalicFont=*-italic,
|
|
122
|
+
BoldItalicFont=*-bolditalic,
|
|
123
|
+
]
|
|
124
|
+
\newcommand{\resume@dates}{}
|
|
125
|
+
% Termes has no U+2197, so draw the arrow.
|
|
126
|
+
\newcommand{\resume@arrow}{\tikz[baseline=-0.1ex]\draw[->, line width=0.5pt] (0,0) -- (0.5em,0.5em);}
|
|
127
|
+
\fi
|
|
128
|
+
|
|
129
|
+
% ---------------------------------------------------------------- page
|
|
130
|
+
\def\resume@letter{letter}
|
|
131
|
+
\ifx\resume@paper\resume@letter
|
|
132
|
+
\PassOptionsToPackage{letterpaper}{geometry}
|
|
133
|
+
\else
|
|
134
|
+
\PassOptionsToPackage{a4paper}{geometry}
|
|
135
|
+
\fi
|
|
136
|
+
\RequirePackage[
|
|
137
|
+
left=\resume@marginLeft,
|
|
138
|
+
right=\resume@marginRight,
|
|
139
|
+
top=\resume@marginTop,
|
|
140
|
+
bottom=\resume@marginBottom,
|
|
141
|
+
nohead
|
|
142
|
+
]{geometry}
|
|
143
|
+
|
|
144
|
+
% ---------------------------------------------------------------- lists
|
|
145
|
+
\RequirePackage{enumitem}
|
|
146
|
+
\setlist{noitemsep}
|
|
147
|
+
\setlist[itemize]{topsep=0.25em, itemsep=\resume@bulletSpacing, leftmargin=\resume@bulletIndent, font=\scriptsize}
|
|
148
|
+
|
|
149
|
+
% ---------------------------------------------------------------- sections
|
|
150
|
+
\RequirePackage{titlesec}
|
|
151
|
+
\ifresume@sansstyle
|
|
152
|
+
% Section labels: small letter-spaced caps (step -1); being small, they need
|
|
153
|
+
% more room above to separate sections. Organisations step up to +1.
|
|
154
|
+
\titleformat{\section}{\resume@labelfont\typestep{-1}\raggedright}{}{0em}{}[\titlerule]
|
|
155
|
+
\titleformat{\subsection}{\typestep{1}\raggedright}{}{0em}{}
|
|
156
|
+
\else
|
|
157
|
+
\titleformat{\section}{\large\raggedright}{}{0em}{}[\titlerule]
|
|
158
|
+
\titleformat{\subsection}{\large\raggedright}{}{0em}{}
|
|
159
|
+
\fi
|
|
160
|
+
\titlespacing*{\section}{0cm}{\resume@sectionBefore}{\resume@sectionAfter}
|
|
161
|
+
\titlespacing*{\subsection}{0cm}{*1.2}{*0.6}
|
|
162
|
+
|
|
163
|
+
% ---------------------------------------------------------------- commands
|
|
164
|
+
% \name{<NAME>} centered header
|
|
165
|
+
\newcommand{\name}[1]{%
|
|
166
|
+
\ifresume@sansstyle
|
|
167
|
+
\centerline{\resume@namefont\typestep{5}#1}%
|
|
168
|
+
\else
|
|
169
|
+
\centerline{\huge{#1}}%
|
|
170
|
+
\fi
|
|
171
|
+
\vspace{1.25ex}%
|
|
172
|
+
}
|
|
173
|
+
% \basicInfo{<contact line>} centered contact line under the name
|
|
174
|
+
\newcommand{\basicInfo}[1]{%
|
|
175
|
+
\ifresume@sansstyle
|
|
176
|
+
\centerline{#1}% supporting information: body size
|
|
177
|
+
\else
|
|
178
|
+
\centerline{\large{#1}}%
|
|
179
|
+
\fi
|
|
180
|
+
\vspace{1.5ex}%
|
|
181
|
+
}
|
|
182
|
+
% \sep middle dot between contact items
|
|
183
|
+
\newcommand{\sep}{\ \textperiodcentered\ }
|
|
184
|
+
% \datedsubsection{<left>}{<dates>} organisation / school row
|
|
185
|
+
\newcommand{\datedsubsection}[2]{%
|
|
186
|
+
\subsection[#1]{#1 \hfill {\resume@dates #2}}%
|
|
187
|
+
}
|
|
188
|
+
% \datedline{<left>}{<right>} role / location row
|
|
189
|
+
\newcommand{\datedline}[2]{%
|
|
190
|
+
{\par #1 \hfill #2 \par}%
|
|
191
|
+
}
|
|
192
|
+
% \role{<title>} job title; italic in serif, upright in sans
|
|
193
|
+
\newcommand{\role}[1]{\textbf{\textit{#1}}}
|
|
194
|
+
% \projectlink{<url>}{<name>} bold project name with a clickable ↗
|
|
195
|
+
\newcommand{\projectlink}[2]{\textbf{\href{#1}{#2\,\resume@arrow}}}
|
|
196
|
+
% plainlist label-less list for education details
|
|
197
|
+
\newenvironment{plainlist}
|
|
198
|
+
{\begin{itemize}[label={}, labelsep=0pt, leftmargin=0pc]}
|
|
199
|
+
{\end{itemize}}
|
|
200
|
+
% Mark one checkable prose paragraph or X-column description. No visual styling.
|
|
201
|
+
\NewDocumentCommand\resumeentry{+m}{\par #1\par}
|
|
202
|
+
|
|
203
|
+
\ifdefined\ResumeMeasure\input{resume-measure.tex}\fi
|