A Kotlin DSL is the single source of truth for this CV. The project renders the same immutable model into:
- a LuaLaTeX document and PDF, at most two pages long;
- a static portfolio site with no intermediate JSON or runtime data fetch.
GitHub Actions verifies the Kotlin code, compiles the PDF, checks its page layout, assembles the site, and deploys it to GitHub Pages: https://tolikttaaa.github.io/personal-cv/.
├── gradle.properties # Pinned cv-dsl release
├── gradle/libs.versions.toml # Build-tool versions
├── my-cv/ # This CV's content and generation entry point
│ └── src/main/
│ ├── kotlin/cv/
│ │ ├── content/ # Header and section definitions
│ │ └── dictionaries/ # Organizations and institutions used by this CV
│ └── resources/photo.jpg # Profile photo
├── build/ # Generated artifacts; never edited manually
└── .github/workflows/ # Verification and Pages deployment
The reusable implementation lives in the separate
tolikttaaa/cv-dsl repository. This
project consumes its tagged JitPack artifact for both the Gradle generation
plugin and the application API; only personal content remains here.
The render layer keeps the model independent from HTML and LaTeX:
CvRendererdefines the publicrender(cv, outputDirectory)operation.RenderFormatis the closed set of supported formats:WebandLatex.CvRendererFactoryselects the complete renderer for a format.RendererBundle<C>is the composition root for a format. It requires a renderer for every supported section and leaf element.SectionRenderer<C>performs exhaustive dispatch over the sealed section hierarchy.ElementRenderer<E, C>is implemented by format-specific renderers such asWebProjectRendererandLatexProjectRenderer.
val renderer = CvRendererFactory.create(RenderFormat.Web)
renderer.render(cv, outputDirectory)CvApplication sits above the individual renderers. It parses the common CLI,
selects formats, owns the output layout, and copies content assets from an
injectable CvAssetSource. The consumer entry point therefore only supplies
its model:
fun main(args: Array<String>) = CvApplication(anatoliiCv).run(args)By default, a photo declared as photo("photo.jpg") is loaded from the
consumer's runtime classpath. Consumers with another asset store can pass a
custom CvAssetSource without changing the generation pipeline.
Both WebRendererBundle and LatexRendererBundle must satisfy the same bundle
contract. If a required renderer is missing, compilation fails. Web renderers
receive WebRenderContext; LaTeX renderers use Unit because section rendering
does not require shared state.
Library architecture, extension rules and required test coverage are documented
in the standalone project's
CONTRIBUTING.md
and architecture.md.
cv-dsl defines the cv.dsl.generation plugin alongside the model and
renderers. This project loads both from one tagged JitPack artifact:
buildscript {
repositories { maven("https://jitpack.io") }
dependencies { classpath("com.github.tolikttaaa:cv-dsl:<tag>") }
}
plugins {
kotlin("jvm")
application
}
apply(plugin = "cv.dsl.generation")
dependencies {
implementation("com.github.tolikttaaa:cv-dsl:<tag>")
}
application {
mainClass.set("cv.MainKt")
}The plugin expects that main class to accept [repositoryRoot, target], where
the target is latex or web. It owns the complete pipeline:
verifyCvEnvironmentchecks LuaLaTeX and the JDKjwebserverexecutable;generateLatexandgenerateWebinvoke the consumer's generator;generatePdfverifies tools, generates LaTeX, runs LuaLaTeX twice, and checks the PDF against its layout rules;assembleSitecombines generated web files and the PDF;serveSiteandstopSitemanage a PID-tracked local preview process.
Optional consumer configuration:
cvGeneration {
mainClass.set("example.MainKt")
lualatexExecutable.set("/opt/texlive/bin/lualatex")
previewPort.set(9090)
}Command-line overrides are also available:
./gradlew generatePdf -PlualatexPath=/absolute/path/to/lualatex
./gradlew serveSite -PcvPreviewPort=9090Missing tools and occupied ports fail early with installation or override
instructions. Preview management uses only JDK APIs; it does not require
bash, lsof, or xargs.
Personal content lives in my-cv/src/main/kotlin/cv/content/:
CvDefinition.ktdefines identity, photo, contacts, section order and footer;- the remaining files define one section each.
Rich text is written as plain content and styled through matching rules:
paragraph(
"""
Software Engineer experienced with Kotlin and ITMO University.
""",
) {
bold("Kotlin")
highlight("ITMO University", linkTo("https://en.itmo.ru/"), bold)
}Supported styles include bold, italic, nowrap, colored(...), and
linkTo(...). A highlight rule that matches nothing fails generation, which
prevents formatting from silently disappearing after text changes. Renderers
escape HTML and LaTeX special characters automatically.
Contacts are declared in visual rows. The web representation flattens those rows into its contact card, while LaTeX preserves the row boundaries:
social {
row {
linkedin("ttaaa")
github("tolikttaaa")
leetcode("ttaaa")
}
}The PDF must stay within two pages, with the whole Experience section on page 1. Both are declared in the content and checked on every PDF build:
pdf {
fontSize = 9.0 // pt; every other size scales with it
maxPages = 2
}
experience(title = "Experience", icon = "faSuitcase", id = "experience", pageFit = PageFit.OnPage(1)) { … }Any section or entry accepts pageFit = PageFit.OnPage(n) (entirely on page
n) or PageFit.SinglePage (never split by a page break). When a change breaks
a rule, generatePdf fails and lists each violation:
PDF layout: 3 pages, 2 of 2 page rules violated:
- The PDF has 3 pages, but is limited to 2
- Experience must fit on page 1, but occupies pages 1–2
build/cv.pdf is still written for inspection, and build/cv-layout.txt shows
the page of every section and entry. Shorten the content or lower fontSize
until the rules hold again.
Requirements:
- JDK matching the
javaversion ingradle/libs.versions.toml; - LuaLaTeX from TeX Live 2022 or newer for PDF generation. Web-only generation does not require a TeX installation.
Important tasks:
| Task | Purpose | Output |
|---|---|---|
check |
Compile, test and run Detekt in both modules | Verification result |
detekt |
Run static analysis only | <module>/build/reports/detekt/ |
verifyCvEnvironment |
Check LuaLaTeX and jwebserver |
Diagnostic output |
generateLatex |
Render the DSL to LaTeX sources | build/latex/ |
generatePdf |
Render and compile the PDF in two passes, check its layout rules | build/cv.pdf, build/cv-layout.txt |
generateWeb |
Render complete HTML and extract browser assets | build/web/ |
assembleSite |
Combine the portfolio, photo and PDF | build/site/ |
serveSite |
Assemble and serve the site on port 8080 | Local HTTP server |
stopSite |
Stop the local server | — |
./gradlew check
./gradlew generatePdf
./gradlew serveSite
./gradlew stopSiteRun ./gradlew tasks --group cv for the CV-specific task list. The application
entry point also supports direct generation:
Main [repository-root] [latex|web|all]
If LuaLaTeX is not installed at the standard MacTeX path and is not available
on PATH, override it with -PlualatexPath=/absolute/path/to/lualatex.
The preview server uses the JDK's jwebserver, records its PID in
build/site-server.pid, and writes output to build/site-server.log.
Changes that need a new library feature can be built against a local checkout
of cv-dsl before it is released:
./gradlew generatePdf -PcvDslPath=../cv-dslThe included build replaces the pinned JitPack artifact on both the plugin classpath and the application classpath.
gradle.properties pins one cvDslVersion for both the build-script plugin
classpath and the application dependency. It normally names a release tag;
update it only after the standalone repository's release CI is green and its
immutable JitPack artifact is available. While a cv-dsl pull request is under
review, a branch here may pin that PR's commit hash instead, which JitPack
builds on demand. Build-tool versions remain in gradle/libs.versions.toml and
follow the versions cv-dsl is built with.
Detekt runs with its default rule set, reports in Checkstyle, HTML, SARIF and
Markdown formats, and is part of every check invocation. Narrow suppressions
are used only for intentionally declarative DSL structures.
Every pull request, push to main and manual dispatch runs, inside a full
TeX Live container:
./gradlew check;- generation of LaTeX and the static portfolio;
- PDF compilation with LuaLaTeX and verification of its layout rules;
- assembly of
build/site.
The PDF, its layout report and the LuaLaTeX log are uploaded as the
cv-layout artifact. Pushes to main then deploy build/site to the
gh-pages branch, which GitHub Pages serves from its root.