diff --git a/.github/workflows/update-files.yaml b/.github/workflows/update-files.yaml deleted file mode 100644 index 434f227a..00000000 --- a/.github/workflows/update-files.yaml +++ /dev/null @@ -1,50 +0,0 @@ -name: Update files - -# GitHub events that trigger the workflow: -on: - workflow_call: # to make the workflow triggerable from other workflows (publish-a-release.yaml) - workflow_dispatch: # to make the workflow triggerable manually - -jobs: - update_files: - name: Update schema.json, examples, and entry figures - runs-on: ubuntu-latest - - permissions: - contents: write - - steps: - - uses: actions/checkout@v6 - - - name: Install uv - uses: astral-sh/setup-uv@v7 - - - name: Install just - uses: taiki-e/install-action@just - - - name: Set Git credentials - run: | - git config --local user.email "github-actions[bot]@users.noreply.github.com" - git config --local user.name "GitHub Actions [Bot]" - - - name: Update schema.json - run: just update-schema - - - name: Update examples folder - run: just update-examples - - - name: Update entry figures - run: just update-entry-figures - - - name: Update llms.txt - run: just update-llms-txt - - - name: Commit and push changes - run: | - git add -A - if git diff --staged --quiet; then - echo "No changes to commit" - else - git commit -m "Update schema.json, examples, llms.txt, and entry figures" - git push origin HEAD:main - fi diff --git a/README.md b/README.md index 1c3eb89d..d0241f2b 100644 --- a/README.md +++ b/README.md @@ -136,6 +136,16 @@ locale: ... ``` +## AI Agent Skill + +Let AI coding agents create and edit your CV. Install the RenderCV skill: + +```bash +npx skills add rendercv/rendercv +``` + +Works with Claude Code, Cursor, Codex, Copilot, Windsurf, Gemini CLI, and [20+ other agents](https://docs.rendercv.com/user_guide/how_to/use_the_ai_agent_skill). + ## Get Started Install RenderCV (Requires Python 3.12+): diff --git a/docs/developer_guide/github_workflows.md b/docs/developer_guide/github_workflows.md index 77a029f8..db269e02 100644 --- a/docs/developer_guide/github_workflows.md +++ b/docs/developer_guide/github_workflows.md @@ -36,7 +36,7 @@ GitHub reads these files and executes them automatically when the triggering eve ## RenderCV's Workflows -RenderCV has 5 workflows. Each handles a specific automation task. +RenderCV has 4 workflows. Each handles a specific automation task. **How workflows start:** Every workflow begins the same way: clone the repository, install `uv`, install `just`, then run some `just` commands. This recreates the same environment you'd have locally (see [Setup](index.md)). @@ -67,21 +67,7 @@ RenderCV has 5 workflows. Each handles a specific automation task. 2. Uploads it to GitHub Pages 3. Documentation is now live at https://docs.rendercv.com -### 3. [`update-files.yaml`](https://github.com/rendercv/rendercv/blob/main/.github/workflows/update-files.yaml): Update Generated Files - -**When it runs:** - -- Manually (via GitHub UI) - -**What it does:** - -1. Regenerates files derived from code: - - `schema.json` using `just update-schema` - - Example YAML files and PDFs in `examples/` folder using `just update-examples` - - Entry figures using `just update-entry-figures` -2. Commits and pushes these changes to the repository - -### 4. [`create-executables.yaml`](https://github.com/rendercv/rendercv/blob/main/.github/workflows/create-executables.yaml): Create Executables +### 3. [`create-executables.yaml`](https://github.com/rendercv/rendercv/blob/main/.github/workflows/create-executables.yaml): Create Executables **When it runs:** @@ -98,7 +84,7 @@ RenderCV has 5 workflows. Each handles a specific automation task. These are single-file executables that users can download and run without installing Python. -### 5. [`release.yaml`](https://github.com/rendercv/rendercv/blob/main/.github/workflows/release.yaml): Publish a Release +### 4. [`release.yaml`](https://github.com/rendercv/rendercv/blob/main/.github/workflows/release.yaml): Publish a Release **When it runs:** diff --git a/docs/index.md b/docs/index.md index 53da54bf..979fd49b 100644 --- a/docs/index.md +++ b/docs/index.md @@ -135,6 +135,16 @@ locale: ... ``` +## AI Agent Skill + +Let AI coding agents create and edit your CV. Install the RenderCV skill: + +```bash +npx skills add rendercv/rendercv +``` + +Works with Claude Code, Cursor, Codex, Copilot, Windsurf, Gemini CLI, and [20+ other agents](https://docs.rendercv.com/user_guide/how_to/use_the_ai_agent_skill). + ## Get Started Install RenderCV (Requires Python 3.12+): diff --git a/docs/llms.txt b/docs/llms.txt new file mode 100644 index 00000000..b436ae3b --- /dev/null +++ b/docs/llms.txt @@ -0,0 +1,805 @@ +--- +name: rendercv +description: >- + Create professional CVs and resumes with perfect typography using RenderCV + (v2.8). Users write content in YAML, and RenderCV produces + publication-quality PDFs via Typst typesetting. Full control over every visual + detail: colors, fonts, margins, spacing, section title styles, entry layouts, + and more. 6 built-in themes with unlimited + customization. Any language supported (20 built-in, or define your own). Outputs + PDF, PNG, HTML, and Markdown. Use when the user wants to create, edit, + customize, or render a CV or resume. +--- + +## Quick Start + +**Available themes:** `classic`, `engineeringclassic`, `engineeringresumes`, `harvard`, `moderncv`, `sb2nov` +**Available locales:** `english`, `arabic`, `danish`, `dutch`, `french`, `german`, `hebrew`, `hindi`, `indonesian`, `italian`, `japanese`, `korean`, `mandarin_chinese`, `norwegian_bokmål`, `norwegian_nynorsk`, `persian`, `portuguese`, `russian`, `spanish`, `turkish` + +These are starting points — every aspect of the design and locale can be fully customized in the YAML file. + +```bash +# Install RenderCV +uv tool install "rendercv[full]" + +# Create a starter YAML file (you can specify theme and locale) +rendercv new "John Doe" +rendercv new "John Doe" --theme moderncv --locale german + +# Render to PDF (also generates Typst, Markdown, HTML, PNG by default) +rendercv render John_Doe_CV.yaml + +# Watch mode: auto-re-render whenever the YAML file changes +rendercv render John_Doe_CV.yaml --watch + +# Render only PNG (useful for previewing or checking page count) +rendercv render John_Doe_CV.yaml --dont-generate-pdf --dont-generate-html --dont-generate-markdown + +# Override fields from the CLI without editing the YAML +rendercv render cv.yaml --cv.name "Jane Doe" --design.theme "moderncv" +``` + +## YAML Structure + +A RenderCV input has four sections. Only `cv` is required — the others have sensible defaults. + +```yaml +cv: # Your content: name, contact info, and all sections +design: # Visual styling: theme, colors, fonts, margins, spacing, layouts +locale: # Language: month names, phrases, translations +settings: # Behavior: output paths, bold keywords, current date +``` + +**Single file vs. separate files:** All four sections can live in one YAML file, or each can be a separate file. Separate files are useful for reusing the same design/locale across multiple CVs: + +```bash +# Single self-contained file (all sections in one file) +rendercv render John_Doe_CV.yaml + +# Separate files: CV content + design + locale loaded independently +rendercv render cv.yaml --design design.yaml --locale-catalog locale.yaml --settings settings.yaml +``` + +When using separate files, each file contains only its section (e.g., `design.yaml` has `design:` as the top-level key). CLI-loaded files override values in the main YAML file. + +The YAML maps directly to Pydantic models. The complete type-safe schema is provided below so you can understand every field, its type, and its default value. + +## Pydantic Schema + +The YAML input is validated against these Pydantic models. + +### Top-Level Model + +```python +class RenderCVModel(BaseModelWithoutExtraKeys): + cv: Cv = pydantic.Field(default_factory=Cv, title='CV', description='The content of the CV.') + design: Design = pydantic.Field(default_factory=ClassicTheme, title='Design') + locale: Locale = pydantic.Field(default_factory=EnglishLocale, title='Locale Catalog') + settings: Settings = pydantic.Field(default_factory=Settings, title='RenderCV Settings', description='The settings of the RenderCV.') + +``` + +### CV Content (`cv`) + +The `cv.sections` field is a dictionary where keys are section titles (any string you want) and values are lists of entries. Each section contains entries of the same type. + +```python +class Cv(BaseModelWithoutExtraKeys): + name: str | None = pydantic.Field(default=None, examples=['John Doe', 'Jane Smith']) + headline: str | None = pydantic.Field(default=None, examples=['Software Engineer', 'Data Scientist', 'Product Manager']) + location: str | None = pydantic.Field(default=None, examples=['New York, NY', 'London, UK', 'Istanbul, Türkiye']) + email: pydantic.EmailStr | list[pydantic.EmailStr] | None = pydantic.Field(default=None, examples=['john.doe@example.com', ['john.doe.1@example.com', 'john.doe.2@example.com']]) + photo: ExistingPathRelativeToInput | pydantic.HttpUrl | None = pydantic.Field(default=None, union_mode='left_to_right', examples=['photo.jpg', 'images/profile.png', 'https://example.com/photo.jpg']) + phone: pydantic_phone_numbers.PhoneNumber | list[pydantic_phone_numbers.PhoneNumber] | None = pydantic.Field(default=None, examples=['+1-234-567-8900', ['+1-234-567-8900', '+44 20 1234 5678']]) + website: pydantic.HttpUrl | list[pydantic.HttpUrl] | None = pydantic.Field(default=None, examples=['https://johndoe.com', ['https://johndoe.com', 'https://www.janesmith.dev']]) + social_networks: list[SocialNetwork] | None = pydantic.Field(default=None) + custom_connections: list[CustomConnection] | None = pydantic.Field(default=None, examples=[[{'placeholder': 'Book a call', 'url': 'https://cal.com/johndoe', 'fontawesome_icon': 'calendar-days'}]]) + sections: dict[str, Section] | None = pydantic.Field(default=None, examples=[{'Experience': '...', 'Education': '...', 'Projects': '...', 'Skills': '...'}]) + +``` + +```python +type SocialNetworkName = Literal['LinkedIn', 'GitHub', 'GitLab', 'IMDB', 'Instagram', 'ORCID', 'Mastodon', 'StackOverflow', 'ResearchGate', 'YouTube', 'Google Scholar', 'Telegram', 'WhatsApp', 'Leetcode', 'X', 'Bluesky', 'Reddit'] + +available_social_networks = get_args(SocialNetworkName.__value__) + +class SocialNetwork(BaseModelWithoutExtraKeys): + network: SocialNetworkName = pydantic.Field() + username: str = pydantic.Field(examples=['john_doe', '@johndoe@mastodon.social', '12345/john-doe']) + +``` + +```python +class CustomConnection(BaseModelWithoutExtraKeys): + fontawesome_icon: str + placeholder: str + url: pydantic.HttpUrl | None + +``` + +### Entry Types + +`cv.sections` is a dictionary: keys are section titles (any string), values are lists of entries. Each section must use a **single** entry type — you cannot mix different entry types within the same section. The entry type is auto-detected from the fields present in each entry. + +**Shared fields** — these are available on entry types that support dates and complex fields (ExperienceEntry, EducationEntry, NormalEntry, PublicationEntry): + +| Field | Type | Default | Notes | +|---|---|---|---| +| `date` | `str \| int \| null` | `null` | Free-form: `"2020-09"`, `"Fall 2023"`, etc. Mutually exclusive with `start_date`/`end_date`. | +| `start_date` | `str \| int \| null` | `null` | Strict format: YYYY-MM-DD, YYYY-MM, or YYYY. | +| `end_date` | `str \| int \| "present" \| null` | `null` | Same formats as `start_date`, or `"present"`. Omitting defaults to `"present"` when `start_date` is set. | +| `location` | `str \| null` | `null` | | +| `summary` | `str \| null` | `null` | | +| `highlights` | `list[str] \| null` | `null` | Bullet points. | + +**9 entry types:** + +| Entry Type | Required Fields | Optional Fields | Typical Use | +|---|---|---|---| +| **ExperienceEntry** | `company`, `position` | all shared fields | Jobs, positions | +| **EducationEntry** | `institution`, `area` | `degree` + all shared fields | Degrees, schools | +| **PublicationEntry** | `title`, `authors` | `doi`, `url`, `journal`, `summary`, `date` | Papers, articles | +| **NormalEntry** | `name` | all shared fields | Projects, awards | +| **OneLineEntry** | `label`, `details` | — | Skills, languages | +| **BulletEntry** | `bullet` | — | Simple bullet points | +| **NumberedEntry** | `number` | — | Numbered list items | +| **ReversedNumberedEntry** | `reversed_number` | — | Reverse-numbered items (5, 4, 3...) | +| **TextEntry** | *(plain string)* | — | Free-form paragraphs | + +Example: + +```yaml +cv: + sections: + experience: # list of ExperienceEntry (detected by company + position) + - company: Google + position: Engineer + start_date: 2020-01 + highlights: + - Did something impactful + skills: # list of OneLineEntry (detected by label + details) + - label: Languages + details: Python, C++ + about_me: # list of TextEntry (plain strings) + - This is a free-form paragraph about me. +``` + +Entries also accept arbitrary extra keys (silently ignored during rendering). A typo in a field name will NOT cause an error. + +### Design (`design`) + +All built-in themes share the same structure — they only differ in default values. See the sample designs below for every available field and its default. Set `design.theme` to pick a theme, then override any field. + +### Locale (`locale`) + +Built-in locales: `english`, `arabic`, `danish`, `dutch`, `french`, `german`, `hebrew`, `hindi`, `indonesian`, `italian`, `japanese`, `korean`, `mandarin_chinese`, `norwegian_bokmål`, `norwegian_nynorsk`, `persian`, `portuguese`, `russian`, `spanish`, `turkish` + +Set `locale.language` to a built-in locale name to use it. Override any field to customize translations. Set `language` to any string and provide all translations for a fully custom locale. + +### Settings (`settings`) + +Key fields: `bold_keywords` (list of strings to auto-bold), `current_date` (override today's date), `render_command.*` (output paths, generation flags). + +## Important Patterns + +### YAML quoting + +**ALWAYS quote string values that contain a colon (`:`).** This is the most common cause of invalid YAML. Highlights, titles, summaries, and any free-form text often contain colons: + +```yaml +# WRONG — colon breaks YAML parsing: +- title: Catalytic Mechanisms: A New Approach + highlights: + - Relevant coursework: Distributed Systems, ML + +# RIGHT — wrap in double quotes: +- title: "Catalytic Mechanisms: A New Approach" + highlights: + - "Relevant coursework: Distributed Systems, ML" +``` + +Rule: if a string value contains `:`, it MUST be quoted. When in doubt, quote it. + +### Bullet characters + +The `design.highlights.bullet` field only accepts these exact characters: `●`, `•`, `◦`, `-`, `◆`, `★`, `■`, `—`, `○`. Do not use en-dash (`–`), `>`, `*`, or any other character. When in doubt, omit `bullet` to use the theme default. + +### Phone numbers + +Phone numbers MUST be in international format with country code (E.164). Never invent a phone number — only include one if the user provides it. + +```yaml +# WRONG: +phone: "(555) 123-4567" +phone: "555-123-4567" + +# RIGHT: +phone: "+15551234567" +``` + +If the user provides a local number without country code, ask which country, or omit the phone field. + +### Text formatting + +All text fields support inline Markdown: `**bold**`, `*italic*`, `[link text](url)`. Block-level Markdown (headers, lists, blockquotes, code blocks) is not supported. Raw Typst commands and math (`$$f(x)$$`) also pass through. + +### Date handling + +- `date` and `start_date`/`end_date` are mutually exclusive. If `date` is provided, `start_date` and `end_date` are ignored. +- If only `start_date` is given, `end_date` defaults to `"present"`. +- `start_date`/`end_date` require strict formats: YYYY-MM-DD, YYYY-MM, or YYYY. +- `date` is flexible: accepts any string ("Fall 2023") in addition to date formats. + +### Section titles + +- `snake_case` keys auto-capitalize: `work_experience` → "Work Experience" +- Keys with spaces or uppercase are used as-is. + +### Publication authors + +Use `*Name*` (single asterisks, italic) to highlight the CV owner in author lists. + +### Nested highlights (sub-bullets) + +```yaml +highlights: + - Main bullet point + - Sub-bullet 1 + - Sub-bullet 2 +``` + +## CLI Reference + +### `rendercv new "Full Name"` + +Generate a starter YAML file. + +| Option | Short | What it does | +|---|---|---| +| `--theme THEME` | | Theme to use (default: `classic`) | +| `--locale LOCALE` | | Locale to use (default: `english`) | +| `--create-typst-templates` | | Also create editable Typst template files for full design control | + +### `rendercv render ` + +Generate PDF, Typst, Markdown, HTML, and PNG from a YAML file. + +| Option | Short | What it does | +|---|---|---| +| `--watch` | `-w` | Re-render automatically when the YAML file changes | +| `--quiet` | `-q` | Suppress all output messages | +| `--design FILE` | `-d` | Load design section from a separate YAML file | +| `--locale-catalog FILE` | `-lc` | Load locale section from a separate YAML file | +| `--settings FILE` | `-s` | Load settings section from a separate YAML file | +| `--output-folder DIR` | `-o` | Custom output directory | + +Per-format controls: `--{format}-path PATH` sets custom output path, `--dont-generate-{format}` skips generation. Formats: `pdf`, `typst`, `markdown`, `html`, `png`. + +**Override any YAML field from the CLI** using dot notation (overrides without editing the file): + +```bash +rendercv render CV.yaml --cv.name "Jane Doe" --design.theme "moderncv" +rendercv render CV.yaml --cv.sections.education.0.institution "MIT" +``` + +### `rendercv create-theme "theme-name"` + +Scaffold a custom theme directory with editable Typst templates for complete design control. + +## JSON Schema + +For YAML editor autocompletion and validation: + +```yaml +# yaml-language-server: $schema=https://raw.githubusercontent.com/rendercv/rendercv/refs/tags/v2.8/schema.json +``` + +## Complete Example + +### Sample CV + +```yaml +cv: + name: John Doe + headline: + location: San Francisco, CA + email: john.doe@email.com + photo: + phone: + website: https://rendercv.com/ + social_networks: + - network: LinkedIn + username: rendercv + - network: GitHub + username: rendercv + custom_connections: + sections: + Welcome to RenderCV: + - RenderCV reads a CV written in a YAML file, and generates a PDF with + professional typography. + - Each section title is arbitrary. + education: + - institution: Princeton University + area: Computer Science + degree: PhD + date: + start_date: 2018-09 + end_date: 2023-05 + location: Princeton, NJ + summary: + highlights: + - 'Thesis: Efficient Neural Architecture Search for Resource-Constrained Deployment' + - 'Advisor: Prof. Sanjeev Arora' + - NSF Graduate Research Fellowship, Siebel Scholar (Class of 2022) + - institution: Boğaziçi University + area: Computer Engineering + degree: BS + date: + start_date: 2014-09 + end_date: 2018-06 + location: Istanbul, Türkiye + summary: + highlights: + - 'GPA: 3.97/4.00, Valedictorian' + - Fulbright Scholarship recipient for Graduate Studies + experience: + - company: Nexus AI + position: Co-Founder & CTO + date: + start_date: 2023-06 + end_date: present + location: San Francisco, CA + summary: + highlights: + - Built foundation model infrastructure serving 2M+ monthly API requests + with 99.97% uptime + - Raised $18M Series A led by Sequoia Capital, with participation from + a16z and Founders Fund + - Scaled engineering team from 3 to 28 across ML research, platform, and + applied AI divisions + - Developed proprietary inference optimization reducing latency by 73% + compared to baseline + - company: NVIDIA Research + position: Research Intern + date: + start_date: 2022-05 + end_date: 2022-08 + location: Santa Clara, CA + summary: + highlights: + - Designed sparse attention mechanism reducing transformer memory + footprint by 4.2x + - Co-authored paper accepted at NeurIPS 2022 (spotlight presentation, top + 5% of submissions) + projects: + - name: '[FlashInfer](https://github.com/)' + date: + start_date: 2023-01 + end_date: present + location: + summary: Open-source library for high-performance LLM inference kernels + highlights: + - Achieved 2.8x speedup over baseline attention implementations on A100 + GPUs + - Adopted by 3 major AI labs, 8,500+ GitHub stars, 200+ contributors + - name: '[NeuralPrune](https://github.com/)' + date: '2021' + start_date: + end_date: + location: + summary: Automated neural network pruning toolkit with differentiable + masks + highlights: + - Reduced model size by 90% with less than 1% accuracy degradation on + ImageNet + - Featured in PyTorch ecosystem tools, 4,200+ GitHub stars + publications: + - title: 'Sparse Mixture-of-Experts at Scale: Efficient Routing for Trillion-Parameter + Models' + authors: + - '*John Doe*' + - Sarah Williams + - David Park + summary: + doi: 10.1234/neurips.2023.1234 + url: + journal: NeurIPS 2023 + date: 2023-07 + - title: Neural Architecture Search via Differentiable Pruning + authors: + - James Liu + - '*John Doe*' + summary: + doi: 10.1234/neurips.2022.5678 + url: + journal: NeurIPS 2022, Spotlight + date: 2022-12 + selected_honors: + - bullet: MIT Technology Review 35 Under 35 Innovators (2024) + - bullet: Forbes 30 Under 30 in Enterprise Technology (2024) + skills: + - label: Languages + details: Python, C++, CUDA, Rust, Julia + - label: ML Frameworks + details: PyTorch, JAX, TensorFlow, Triton, ONNX + patents: + - number: Adaptive Quantization for Neural Network Inference on Edge Devices + (US Patent 11,234,567) + - number: Dynamic Sparsity Patterns for Efficient Transformer Attention (US + Patent 11,345,678) + invited_talks: + - reversed_number: Scaling Laws for Efficient Inference — Stanford HAI + Symposium (2024) + - reversed_number: Building AI Infrastructure for the Next Decade — + TechCrunch Disrupt (2024) + +``` + +### Sample Design (classic — complete reference) + +This shows every available design field with its default value. All themes share the same structure. + +```yaml +design: + theme: classic + page: + size: us-letter + top_margin: 0.7in + bottom_margin: 0.7in + left_margin: 0.7in + right_margin: 0.7in + show_footer: true + show_top_note: true + colors: + body: rgb(0, 0, 0) + name: rgb(0, 79, 144) + headline: rgb(0, 79, 144) + connections: rgb(0, 79, 144) + section_titles: rgb(0, 79, 144) + links: rgb(0, 79, 144) + footer: rgb(128, 128, 128) + top_note: rgb(128, 128, 128) + typography: + line_spacing: 0.6em + alignment: justified + date_and_location_column_alignment: right + font_family: + body: Source Sans 3 + name: Source Sans 3 + headline: Source Sans 3 + connections: Source Sans 3 + section_titles: Source Sans 3 + font_size: + body: 10pt + name: 30pt + headline: 10pt + connections: 10pt + section_titles: 1.4em + small_caps: + name: false + headline: false + connections: false + section_titles: false + bold: + name: true + headline: false + connections: false + section_titles: true + links: + underline: false + show_external_link_icon: false + header: + alignment: center + photo_width: 3.5cm + photo_position: left + photo_space_left: 0.4cm + photo_space_right: 0.4cm + space_below_name: 0.7cm + space_below_headline: 0.7cm + space_below_connections: 0.7cm + connections: + phone_number_format: national + hyperlink: true + show_icons: true + display_urls_instead_of_usernames: false + separator: '' + space_between_connections: 0.5cm + section_titles: + type: with_partial_line + line_thickness: 0.5pt + space_above: 0.5cm + space_below: 0.3cm + sections: + allow_page_break: true + space_between_regular_entries: 1.2em + space_between_text_based_entries: 0.3em + show_time_spans_in: + - experience + entries: + date_and_location_width: 4.15cm + side_space: 0.2cm + space_between_columns: 0.1cm + allow_page_break: false + short_second_row: true + degree_width: 1cm + summary: + space_above: 0cm + space_left: 0cm + highlights: + bullet: • + nested_bullet: • + space_left: 0.15cm + space_above: 0cm + space_between_items: 0cm + space_between_bullet_and_text: 0.5em + templates: + footer: '*NAME -- PAGE_NUMBER/TOTAL_PAGES*' + top_note: '*LAST_UPDATED CURRENT_DATE*' + single_date: MONTH_ABBREVIATION YEAR + date_range: START_DATE – END_DATE + time_span: HOW_MANY_YEARS YEARS HOW_MANY_MONTHS MONTHS + one_line_entry: + main_column: '**LABEL:** DETAILS' + education_entry: + main_column: |- + **INSTITUTION**, AREA + SUMMARY + HIGHLIGHTS + degree_column: '**DEGREE**' + date_and_location_column: |- + LOCATION + DATE + normal_entry: + main_column: |- + **NAME** + SUMMARY + HIGHLIGHTS + date_and_location_column: |- + LOCATION + DATE + experience_entry: + main_column: |- + **COMPANY**, POSITION + SUMMARY + HIGHLIGHTS + date_and_location_column: |- + LOCATION + DATE + publication_entry: + main_column: |- + **TITLE** + SUMMARY + AUTHORS + URL (JOURNAL) + date_and_location_column: DATE + +``` + +### Other Theme Overrides + +Other themes only override specific fields from the classic defaults above. To use a theme, set `design.theme` and optionally override any field. Each theme also customizes `design.templates` (entry layout patterns) — see the classic sample above for the full template structure. The override YAMLs below omit templates for brevity. + +#### engineeringclassic + +```yaml +# yaml-language-server: $schema=../../../../../../schema.json +design: + theme: engineeringclassic + typography: + font_family: + body: Raleway + name: Raleway + headline: Raleway + connections: Raleway + section_titles: Raleway + bold: + name: false + section_titles: false + header: + alignment: left + links: + show_external_link_icon: false + section_titles: + type: with_full_line + sections: + show_time_spans_in: [] + entries: + short_second_row: false + summary: + space_above: 0.12cm + highlights: + space_left: 0cm + space_above: 0.12cm + space_between_items: 0.12cm +``` + +#### engineeringresumes + +```yaml +# yaml-language-server: $schema=../../../../../../schema.json +design: + theme: engineeringresumes + page: + show_footer: false + typography: + font_family: + body: XCharter + name: XCharter + headline: XCharter + connections: XCharter + section_titles: XCharter + font_size: + name: 25pt + section_titles: 1.2em + bold: + name: false + header: + connections: + separator: '|' + show_icons: false + display_urls_instead_of_usernames: true + colors: + name: rgb(0,0,0) + connections: rgb(0,0,0) + headline: rgb(0,0,0) + section_titles: rgb(0,0,0) + links: rgb(0,0,0) + links: + underline: true + show_external_link_icon: false + section_titles: + type: with_full_line + space_above: 0.5cm + space_below: 0.3cm + sections: + space_between_regular_entries: 0.42cm + space_between_text_based_entries: 0.15cm + show_time_spans_in: [] + entries: + short_second_row: false + summary: + space_above: 0.08cm + side_space: 0cm + highlights: + bullet: ● + nested_bullet: ● + space_left: 0cm + space_above: 0.08cm + space_between_items: 0.08cm + space_between_bullet_and_text: 0.3em +``` + +#### harvard + +```yaml +# yaml-language-server: $schema=../../../../../../schema.json +design: + theme: harvard + page: + top_margin: 0.5in + bottom_margin: 0.5in + left_margin: 0.5in + right_margin: 0.5in + show_top_note: false + colors: + name: rgb(0,0,0) + headline: rgb(0,0,0) + connections: rgb(0,0,0) + section_titles: rgb(0,0,0) + links: rgb(0,0,0) + typography: + font_family: + body: XCharter + name: XCharter + headline: XCharter + connections: XCharter + section_titles: XCharter + font_size: + name: 25pt + connections: 9pt + section_titles: 1.3em + header: + space_below_name: 0.5cm + space_below_headline: 0.5cm + space_below_connections: 0.5cm + connections: + show_icons: false + separator: • + space_between_connections: 0.4cm + section_titles: + type: centered_with_centered_partial_line + space_below: 0.2cm + sections: + space_between_regular_entries: 1em + show_time_spans_in: [] + entries: + short_second_row: false +``` + +#### moderncv + +```yaml +# yaml-language-server: $schema=../../../../../../schema.json +design: + theme: moderncv + typography: + line_spacing: 0.6em + font_family: + body: Fontin + name: Fontin + headline: Fontin + connections: Fontin + section_titles: Fontin + font_size: + name: 25pt + section_titles: 1.4em + bold: + name: false + section_titles: false + header: + alignment: left + photo_width: 4.15cm + photo_space_left: 0cm + photo_space_right: 0.3cm + links: + underline: true + show_external_link_icon: false + section_titles: + type: moderncv + space_above: 0.55cm + space_below: 0.3cm + line_thickness: 0.15cm + sections: + show_time_spans_in: [] + entries: + short_second_row: false + side_space: 0cm + space_between_columns: 0.3cm + summary: + space_above: 0.1cm + highlights: + space_left: 0cm + space_above: 0.15cm + space_between_items: 0.1cm + space_between_bullet_and_text: 0.3em +``` + +#### sb2nov + +```yaml +# yaml-language-server: $schema=../../../../../../schema.json +design: + theme: sb2nov + typography: + font_family: + body: New Computer Modern + name: New Computer Modern + headline: New Computer Modern + connections: New Computer Modern + section_titles: New Computer Modern + colors: + name: rgb(0,0,0) + connections: rgb(0,0,0) + section_titles: rgb(0,0,0) + headline: rgb(0,0,0) + links: rgb(0,0,0) + links: + underline: true + show_external_link_icon: false + section_titles: + type: with_full_line + sections: + show_time_spans_in: [] + header: + connections: + hyperlink: true + show_icons: false + display_urls_instead_of_usernames: true + separator: • + entries: + short_second_row: false + highlights: + bullet: ◦ + nested_bullet: ◦ +``` + diff --git a/docs/user_guide/how_to/use_the_ai_agent_skill.md b/docs/user_guide/how_to/use_the_ai_agent_skill.md new file mode 100644 index 00000000..4f3e506e --- /dev/null +++ b/docs/user_guide/how_to/use_the_ai_agent_skill.md @@ -0,0 +1,69 @@ +# Use the AI Agent Skill + +RenderCV provides an AI agent skill that teaches AI coding assistants how to create, edit, and render CVs. Once installed, your agent gains full knowledge of RenderCV's YAML schema, CLI commands, themes, locales, and design options. + +## Supported Agents + +The skill works with any AI coding agent that supports the skills protocol, including: + +- Claude Code +- Cursor +- Codex +- Copilot +- Windsurf +- Gemini CLI +- and 20+ others + +## Install the Skill + +=== "Vercel Skills CLI" + + ```bash + npx skills add rendercv/rendercv + ``` + + You can also target a specific agent: + + ```bash + npx skills add rendercv/rendercv -a claude-code + npx skills add rendercv/rendercv -a cursor + npx skills add rendercv/rendercv -a codex + ``` + +=== "OpenSkills" + + ```bash + npx openskills install rendercv/rendercv + ``` + +=== "Manual" + + Copy the content of [`skills/rendercv/SKILL.md`](https://github.com/rendercv/rendercv/blob/main/skills/rendercv/SKILL.md) into your agent's skill directory. For example, for Claude Code: + + ```bash + git clone https://github.com/rendercv/rendercv.git + cp -r rendercv/skills/rendercv ~/.claude/skills/ + ``` + +## What the Skill Provides + +The skill gives your AI agent: + +- Full knowledge of RenderCV's YAML input structure +- All 6 built-in themes and their design options +- Complete locale and language support (20 built-in languages) +- CLI commands and their options +- Pydantic model schemas for precise field types and defaults +- A complete sample CV as a reference + +## Usage + +After installing the skill, simply ask your AI agent to work with your CV: + +- "Create a new CV for me using the classic theme" +- "Switch my CV to the engineeringresumes theme" +- "Add a new experience entry to my CV" +- "Change the font to Source Sans 3 and make the margins smaller" +- "Translate my CV to German" + +The agent will use its knowledge from the skill to produce correct YAML and run the right `rendercv` commands. diff --git a/examples/John_Doe_ClassicTheme_CV.yaml b/examples/John_Doe_ClassicTheme_CV.yaml index 1b002f0c..6d481a62 100644 --- a/examples/John_Doe_ClassicTheme_CV.yaml +++ b/examples/John_Doe_ClassicTheme_CV.yaml @@ -1,4 +1,4 @@ -# yaml-language-server: $schema=https://raw.githubusercontent.com/rendercv/rendercv/refs/tags/v2.7/schema.json +# yaml-language-server: $schema=https://raw.githubusercontent.com/rendercv/rendercv/refs/tags/v2.8/schema.json cv: name: John Doe headline: diff --git a/examples/John_Doe_EngineeringclassicTheme_CV.yaml b/examples/John_Doe_EngineeringclassicTheme_CV.yaml index 073dacc6..fbe4863f 100644 --- a/examples/John_Doe_EngineeringclassicTheme_CV.yaml +++ b/examples/John_Doe_EngineeringclassicTheme_CV.yaml @@ -1,4 +1,4 @@ -# yaml-language-server: $schema=https://raw.githubusercontent.com/rendercv/rendercv/refs/tags/v2.7/schema.json +# yaml-language-server: $schema=https://raw.githubusercontent.com/rendercv/rendercv/refs/tags/v2.8/schema.json cv: name: John Doe headline: diff --git a/examples/John_Doe_EngineeringresumesTheme_CV.yaml b/examples/John_Doe_EngineeringresumesTheme_CV.yaml index a3f554fe..f3ab0f25 100644 --- a/examples/John_Doe_EngineeringresumesTheme_CV.yaml +++ b/examples/John_Doe_EngineeringresumesTheme_CV.yaml @@ -1,4 +1,4 @@ -# yaml-language-server: $schema=https://raw.githubusercontent.com/rendercv/rendercv/refs/tags/v2.7/schema.json +# yaml-language-server: $schema=https://raw.githubusercontent.com/rendercv/rendercv/refs/tags/v2.8/schema.json cv: name: John Doe headline: diff --git a/examples/John_Doe_ModerncvTheme_CV.yaml b/examples/John_Doe_ModerncvTheme_CV.yaml index 6087830f..6cbac187 100644 --- a/examples/John_Doe_ModerncvTheme_CV.yaml +++ b/examples/John_Doe_ModerncvTheme_CV.yaml @@ -1,4 +1,4 @@ -# yaml-language-server: $schema=https://raw.githubusercontent.com/rendercv/rendercv/refs/tags/v2.7/schema.json +# yaml-language-server: $schema=https://raw.githubusercontent.com/rendercv/rendercv/refs/tags/v2.8/schema.json cv: name: John Doe headline: diff --git a/examples/John_Doe_Sb2novTheme_CV.yaml b/examples/John_Doe_Sb2novTheme_CV.yaml index 4e74f575..d13263c9 100644 --- a/examples/John_Doe_Sb2novTheme_CV.yaml +++ b/examples/John_Doe_Sb2novTheme_CV.yaml @@ -1,4 +1,4 @@ -# yaml-language-server: $schema=https://raw.githubusercontent.com/rendercv/rendercv/refs/tags/v2.7/schema.json +# yaml-language-server: $schema=https://raw.githubusercontent.com/rendercv/rendercv/refs/tags/v2.8/schema.json cv: name: John Doe headline: diff --git a/justfile b/justfile index 3e95a9fa..bcb179c5 100644 --- a/justfile +++ b/justfile @@ -44,6 +44,9 @@ update-schema: update-examples: uv run --frozen --all-extras scripts/update_examples.py +update-skill: + uv run --frozen --all-extras scripts/rendercv_skill/generate.py + update-entry-figures: uv run --frozen --all-extras --group update-entry-figures scripts/update_entry_figures.py diff --git a/mkdocs.yaml b/mkdocs.yaml index 66cf6afa..2a21c730 100644 --- a/mkdocs.yaml +++ b/mkdocs.yaml @@ -66,6 +66,7 @@ nav: - Arbitrary Keys in Entries: user_guide/how_to/arbitrary_keys_in_entries.md - Custom Fonts: user_guide/how_to/custom_fonts.md - Override Default Templates: user_guide/how_to/override_default_templates.md + - Use the AI Agent Skill: user_guide/how_to/use_the_ai_agent_skill.md - Developer Guide: - Setup: developer_guide/index.md - Project Management: developer_guide/project_management.md diff --git a/scripts/rendercv_skill/evals/fixtures/sample_cv_text.txt b/scripts/rendercv_skill/evals/fixtures/sample_cv_text.txt new file mode 100644 index 00000000..5bbc1f5a --- /dev/null +++ b/scripts/rendercv_skill/evals/fixtures/sample_cv_text.txt @@ -0,0 +1,20 @@ +SARAH JOHNSON +Software Developer | San Francisco, CA | sarah.j@email.com | linkedin.com/in/sarahj + +EXPERIENCE +Meta, Software Engineer, Jan 2022 - Present, Menlo Park, CA +- Built real-time data pipeline processing 10M events/day +- Led migration from monolith to microservices architecture + +Stripe, Junior Developer, Jun 2020 - Dec 2021, San Francisco, CA +- Developed payment integration APIs used by 500+ merchants +- Reduced API response time by 40% + +EDUCATION +University of Washington, B.S. Computer Science, 2016 - 2020, Seattle, WA +- GPA: 3.85/4.0, Dean's List + +SKILLS +Languages: Python, Java, Go, TypeScript +Frameworks: React, Django, gRPC +Tools: AWS, Docker, Kubernetes, Terraform diff --git a/scripts/rendercv_skill/evals/graders/rendercv_validation.py b/scripts/rendercv_skill/evals/graders/rendercv_validation.py new file mode 100644 index 00000000..29dc2ae4 --- /dev/null +++ b/scripts/rendercv_skill/evals/graders/rendercv_validation.py @@ -0,0 +1,67 @@ +"""Deterministic grader: extract YAML from output, validate with RenderCV's pipeline. + +Why: + Every RenderCV skill response that includes YAML should produce valid, + schema-compliant YAML. This grader uses RenderCV's own pydantic validation + (the same pipeline as `rendercv render`) to catch syntax errors and schema + violations before the LLM-as-judge even runs. +""" + +import re + +from rendercv.exception import RenderCVUserValidationError +from rendercv.schema.rendercv_model_builder import ( + build_rendercv_model_from_commented_map, + read_yaml_with_validation_errors, +) + + +def extract_yaml_blocks(text: str) -> list[str]: + """Extract YAML code blocks from markdown output.""" + return re.findall(r"```ya?ml\s*\n(.*?)```", text, re.DOTALL) + + +def get_assert(output: str, context: dict) -> dict: # noqa: ARG001 + """Validate that output contains a parseable, schema-valid RenderCV YAML block. + + Args: + output: The LLM's text response. + context: promptfoo context (required by promptfoo, unused here). + + Returns: + GradingResult dict with pass, score, and reason. + """ + yaml_blocks = extract_yaml_blocks(output) + + if not yaml_blocks: + return { + "pass": False, + "score": 0, + "reason": "No YAML code block found in output.", + } + + for i, block in enumerate(yaml_blocks): + try: + commented_map = read_yaml_with_validation_errors(block, "main_yaml_file") + build_rendercv_model_from_commented_map(commented_map) + except RenderCVUserValidationError as e: + error_messages = "; ".join(err.message for err in e.validation_errors) + return { + "pass": False, + "score": 0, + "reason": ( + f"YAML block {i + 1} fails RenderCV validation: {error_messages}" + ), + } + + return { + "pass": True, + "score": 1, + "reason": "Valid RenderCV YAML.", + } + + return { + "pass": False, + "score": 0, + "reason": "No YAML code block found in output.", + } diff --git a/scripts/rendercv_skill/evals/promptfooconfig.yaml b/scripts/rendercv_skill/evals/promptfooconfig.yaml new file mode 100644 index 00000000..9099a158 --- /dev/null +++ b/scripts/rendercv_skill/evals/promptfooconfig.yaml @@ -0,0 +1,16 @@ +description: RenderCV Skill Evaluation + +providers: + - id: anthropic:messages:claude-sonnet-4-6 + config: + temperature: 0.0 + max_tokens: 4096 + +prompts: + - file://prompts/skill_chat.js + +tests: + - file://tests/cv_generation.yaml + - file://tests/design.yaml + - file://tests/cli_workflow.yaml + - file://tests/pdf_parsing.yaml diff --git a/scripts/rendercv_skill/evals/prompts/skill_chat.js b/scripts/rendercv_skill/evals/prompts/skill_chat.js new file mode 100644 index 00000000..bc760349 --- /dev/null +++ b/scripts/rendercv_skill/evals/prompts/skill_chat.js @@ -0,0 +1,24 @@ +import fs from "fs"; +import path from "path"; +import { fileURLToPath } from "url"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +const skillPath = path.resolve( + __dirname, + "..", + "..", + "..", + "..", + "skills", + "rendercv", + "SKILL.md", +); +const skillContent = fs.readFileSync(skillPath, "utf-8"); + +export default function (context) { + return [ + { role: "system", content: skillContent }, + { role: "user", content: context.vars.query }, + ]; +} diff --git a/scripts/rendercv_skill/evals/tests/cli_workflow.yaml b/scripts/rendercv_skill/evals/tests/cli_workflow.yaml new file mode 100644 index 00000000..44832224 --- /dev/null +++ b/scripts/rendercv_skill/evals/tests/cli_workflow.yaml @@ -0,0 +1,42 @@ +- description: "Install command uses rendercv[full]" + vars: + query: "How do I install RenderCV?" + assert: + - type: contains + value: "rendercv[full]" + +- description: "Separate design file workflow" + vars: + query: "I want to keep my CV content and design in separate YAML files so I can reuse the same design across multiple CVs. How do I set this up?" + assert: + - type: contains + value: "--design" + - type: llm-rubric + value: | + The response explains: + - The main YAML file contains cv content (and optionally locale/settings), but no design + - A separate YAML file contains the design configuration under the design: key + - Use 'rendercv render cv.yaml --design design.yaml' to combine them + +- description: "Watch mode for live editing" + vars: + query: "How can I see my CV update in real-time as I edit the YAML file?" + assert: + - type: contains + value: "--watch" + +- description: "CLI field overrides with dot notation" + vars: + query: "I want to render my CV but temporarily change my name to 'Jane Smith' and switch to the moderncv theme without editing the YAML file. How?" + assert: + - type: contains + value: "--cv.name" + - type: contains + value: "--design.theme" + +- description: "Bold keywords in settings" + vars: + query: "I want RenderCV to automatically bold certain keywords in my CV: 'Python', 'Machine Learning', and 'Kubernetes'. How do I set this up?" + assert: + - type: contains + value: "bold_keywords" diff --git a/scripts/rendercv_skill/evals/tests/cv_generation.yaml b/scripts/rendercv_skill/evals/tests/cv_generation.yaml new file mode 100644 index 00000000..37dbe49a --- /dev/null +++ b/scripts/rendercv_skill/evals/tests/cv_generation.yaml @@ -0,0 +1,73 @@ +- description: "Software engineer CV" + vars: + query: "Create a complete RenderCV YAML file for a software engineer named Alice Chen. She has 5 years of experience: 3 years at Google as a Senior Software Engineer and 2 years at Microsoft as a Software Engineer. She has a CS degree from Stanford (BS, 2017-2021). Her skills include Python, Go, Kubernetes, and React. She has two side projects: an open-source CLI tool and a personal blog engine." + assert: + - type: python + value: file://graders/rendercv_validation.py + - type: llm-rubric + value: | + The output contains a RenderCV YAML with: + - cv.name set to Alice Chen + - Experience entries for Google and Microsoft with correct roles + - Education at Stanford + - A skills section + - A projects section + +- description: "Academic researcher CV" + vars: + query: "Create a RenderCV YAML file for Dr. Maria Rodriguez, a physics professor at MIT. She has a PhD from Caltech (2012-2017) and a BS from Berkeley (2008-2012). She has published 3 papers in Nature and Physical Review Letters. She was a postdoc at CERN (2017-2020) before joining MIT as assistant professor (2020-present). Include a grants section with two NSF grants." + assert: + - type: python + value: file://graders/rendercv_validation.py + - type: llm-rubric + value: | + The output contains: + - cv.name is Dr. Maria Rodriguez + - A publications section with 3 papers + - Education at Caltech and Berkeley + - Experience for postdoc at CERN and professor at MIT + - A grants section with two NSF grants + +- description: "Fresh graduate CV" + vars: + query: "Create a RenderCV YAML file for James Park, a fresh CS graduate from UC Berkeley (2020-2024, GPA 3.8). He has no work experience but has 3 class projects and relevant coursework. His skills are Python, Java, SQL, and machine learning. Use the engineeringresumes theme." + assert: + - type: python + value: file://graders/rendercv_validation.py + - type: contains + value: "engineeringresumes" + - type: llm-rubric + value: | + The output: + - Uses engineeringresumes theme + - Has education at UC Berkeley + - Has a projects section with 3 projects + - Has a skills section + - Does NOT have an experience section (fresh graduate) + +- description: "Non-English CV with French locale" + vars: + query: "Create a RenderCV YAML file for Pierre Dupont, an engineer in Paris. He works at Airbus as a mechanical engineer (2019-present). He studied at Ecole Polytechnique (2014-2019). Use the French locale and the classic theme." + assert: + - type: python + value: file://graders/rendercv_validation.py + - type: llm-rubric + value: | + The output: + - Sets locale language to french + - Has cv.name as Pierre Dupont + - Has experience at Airbus + - Has education at Ecole Polytechnique + +- description: "Minimal CV" + vars: + query: "Create the simplest possible RenderCV YAML file for someone named Sam Lee with just their name and a skills section listing 'Python, JavaScript, SQL'." + assert: + - type: python + value: file://graders/rendercv_validation.py + - type: llm-rubric + value: | + The output is minimal: + - cv.name is Sam Lee + - Has a skills section with Python, JavaScript, SQL + - Does not have unnecessary empty sections diff --git a/scripts/rendercv_skill/evals/tests/design.yaml b/scripts/rendercv_skill/evals/tests/design.yaml new file mode 100644 index 00000000..7de7532a --- /dev/null +++ b/scripts/rendercv_skill/evals/tests/design.yaml @@ -0,0 +1,23 @@ +- description: "Change theme to moderncv" + vars: + query: "Create a simple RenderCV YAML for John Doe with just a name and one skill. Use the moderncv theme." + assert: + - type: python + value: file://graders/rendercv_validation.py + - type: contains + value: "moderncv" + +- description: "Custom colors and margins" + vars: + query: "Create a RenderCV YAML for John Doe with just a name and one skill. Use dark red (#8B0000) for the name and section titles, 1-inch margins on all sides, and A4 paper. Keep the classic theme." + assert: + - type: python + value: file://graders/rendercv_validation.py + - type: llm-rubric + value: | + The output includes a design section with: + - design.colors.name set to a red color (like #8B0000 or rgb equivalent) + - design.colors.section_titles set to the same red color + - Margins set to 1in + - design.page.size set to 'a4' + - theme remains 'classic' diff --git a/scripts/rendercv_skill/evals/tests/pdf_parsing.yaml b/scripts/rendercv_skill/evals/tests/pdf_parsing.yaml new file mode 100644 index 00000000..ff7eeda0 --- /dev/null +++ b/scripts/rendercv_skill/evals/tests/pdf_parsing.yaml @@ -0,0 +1,109 @@ +- description: "Parse simple one-page CV text" + vars: + query: | + Convert this CV text into a RenderCV YAML file: + + SARAH JOHNSON + Software Developer | San Francisco, CA | sarah.j@email.com | linkedin.com/in/sarahj + + EXPERIENCE + Meta, Software Engineer, Jan 2022 - Present, Menlo Park, CA + - Built real-time data pipeline processing 10M events/day + - Led migration from monolith to microservices architecture + + Stripe, Junior Developer, Jun 2020 - Dec 2021, San Francisco, CA + - Developed payment integration APIs used by 500+ merchants + - Reduced API response time by 40% + + EDUCATION + University of Washington, B.S. Computer Science, 2016 - 2020, Seattle, WA + - GPA: 3.85/4.0, Dean's List + + SKILLS + Languages: Python, Java, Go, TypeScript + Frameworks: React, Django, gRPC + Tools: AWS, Docker, Kubernetes, Terraform + assert: + - type: python + value: file://graders/rendercv_validation.py + - type: llm-rubric + value: | + The output correctly parses the CV text into RenderCV YAML: + - cv.name is Sarah Johnson + - cv.location is San Francisco, CA + - cv.email is sarah.j@email.com + - Experience includes Meta and Stripe with bullet points preserved + - Education includes University of Washington + - Skills include Languages, Frameworks, Tools categories + - LinkedIn is included in social_networks + +- description: "Parse academic CV with publications" + vars: + query: | + Convert this academic CV into RenderCV YAML: + + DR. ELENA PETROV + Associate Professor of Chemistry + Stanford University | elena.petrov@stanford.edu + + ACADEMIC POSITIONS + Stanford University, Associate Professor, 2020 - Present + Stanford University, Assistant Professor, 2015 - 2020 + MIT, Postdoctoral Fellow, 2013 - 2015 + + EDUCATION + University of Cambridge, PhD Chemistry, 2009 - 2013 + Moscow State University, BSc Chemistry, 2005 - 2009 + + SELECTED PUBLICATIONS + 1. "Catalytic Mechanisms in Organic Synthesis" - E. Petrov, J. Smith, Nature Chemistry, 2023 + 2. "Novel Metal-Organic Frameworks for CO2 Capture" - E. Petrov, A. Lee, K. Wang, JACS, 2021 + 3. "Photocatalytic Water Splitting" - M. Brown, E. Petrov, Science, 2019 + + GRANTS + NSF CAREER Award ($750,000), 2018 - 2023 + DOE Early Career Research Grant ($500,000), 2016 - 2020 + assert: + - type: python + value: file://graders/rendercv_validation.py + - type: llm-rubric + value: | + The output correctly handles the academic CV: + - cv.name is Dr. Elena Petrov or Elena Petrov + - Publications section has 3 entries with titles and authors + - Academic positions are included (Stanford, MIT) + - Education includes Cambridge and Moscow State + - Grants section is present + +- description: "Parse messy CV with inconsistent formatting" + vars: + query: | + Please convert this poorly formatted CV into clean RenderCV YAML: + + Bob Martinez + bob@gmail.com, (650) 321-7654 + + Work History: + * Senior Dev at Amazon (2021-now) - Seattle + Built recommendation engine + Managed team of 5 + * Developer, Netflix, from July 2018 to December 2020, in Los Gatos + - Worked on streaming backend + * Intern @ Google summer 2017 + + School: + UC Berkeley - CS degree (graduated 2018) + + I know: Python, Java, C++, and also React and Node.js + assert: + - type: python + value: file://graders/rendercv_validation.py + - type: llm-rubric + value: | + Despite the messy input, the output produces clean RenderCV YAML: + - cv.name is Bob Martinez + - Experience includes Amazon, Netflix, and Google + - Amazon has end_date 'present' (from "now") + - Education includes UC Berkeley + - Skills are structured properly + - Email is included diff --git a/scripts/rendercv_skill/generate.py b/scripts/rendercv_skill/generate.py new file mode 100755 index 00000000..3f05806e --- /dev/null +++ b/scripts/rendercv_skill/generate.py @@ -0,0 +1,308 @@ +#!/usr/bin/env python3 +"""Generate the distributable AI agent skill SKILL.md from a Jinja2 template. + +Why: + The SKILL.md file contains dynamic data (themes, locales, Pydantic model + source code, etc.) that must stay in sync with the rendercv package. This + script extracts that data and renders the Jinja2 template to produce an + always-current skill file. +""" + +import ast +import io +import pathlib +import re + +import jinja2 +import ruamel.yaml + +from rendercv import __version__ +from rendercv.schema.models.design.built_in_design import available_themes +from rendercv.schema.models.locale.locale import available_locales +from rendercv.schema.sample_generator import ( + create_sample_cv_file, + create_sample_design_file, +) + +repository_root = pathlib.Path(__file__).parent.parent.parent +script_directory = pathlib.Path(__file__).parent +template_path = script_directory / "skill_template.j2.md" +output_path = repository_root / "skills" / "rendercv" / "SKILL.md" +llms_txt_path = repository_root / "docs" / "llms.txt" + +# Paths to key model source files for dynamic inclusion. +models_dir = repository_root / "src" / "rendercv" / "schema" / "models" + +# These source files are included in the skill so agents can see the +# type-safe Pydantic schema for core models. +MODEL_SOURCE_FILES: dict[str, pathlib.Path] = { + "rendercv_model": models_dir / "rendercv_model.py", + "cv": models_dir / "cv" / "cv.py", + "social_network": models_dir / "cv" / "social_network.py", + "custom_connection": models_dir / "cv" / "custom_connection.py", +} + + +def is_type_adapter_assignment(node: ast.Assign) -> bool: + """Check if an assignment creates a pydantic.TypeAdapter instance. + + Why: + Type adapter instances (e.g., `email_validator = pydantic.TypeAdapter[...]`) + are internal validation plumbing, not part of the user-facing schema. + + Args: + node: An AST Assign node. + + Returns: + True if the assignment is a TypeAdapter instantiation. + """ + source = ast.unparse(node) + return "TypeAdapter" in source + + +def is_private_attr(node: ast.AnnAssign) -> bool: + """Check if an annotated assignment is a private attribute. + + Why: + Private attributes (e.g., `_key_order: list[str] = pydantic.PrivateAttr()`) + are internal state, not user-facing YAML fields. + + Args: + node: An AST AnnAssign node. + + Returns: + True if the field name starts with underscore. + """ + target = ast.unparse(node.target) + return target.startswith("_") + + +def is_model_config(node: ast.Assign) -> bool: + """Check if an assignment is a model_config declaration. + + Args: + node: An AST Assign node. + + Returns: + True if assigning to model_config. + """ + return any(ast.unparse(t) == "model_config" for t in node.targets) + + +def is_url_dictionary(node: ast.AnnAssign) -> bool: + """Check if an annotated assignment is the social network URL dictionary. + + Why: + The url_dictionary mapping (network name → URL prefix) is internal + plumbing. Agents only need the SocialNetworkName type alias to know + which networks are supported. + + Args: + node: An AST AnnAssign node. + + Returns: + True if the target is url_dictionary. + """ + return ast.unparse(node.target) == "url_dictionary" + + +def strip_class_body(class_node: ast.ClassDef) -> ast.ClassDef: + """Strip a class definition down to just field definitions. + + Why: + Agents need field names, types, and defaults — not validators, + serializers, or cached properties. Stripping methods removes + ~50% of tokens from model-heavy files. + + Args: + class_node: An AST ClassDef node. + + Returns: + A new ClassDef with only field annotations (no methods). + """ + new_body: list[ast.stmt] = [] + for child in class_node.body: + if (isinstance(child, ast.AnnAssign) and not is_private_attr(child)) or ( + isinstance(child, ast.Assign) and not is_model_config(child) + ): + new_body.append(child) + + # If the body would be empty, add a `pass` statement + if not new_body: + new_body.append(ast.Pass()) + + new_class = ast.ClassDef( + name=class_node.name, + bases=class_node.bases, + keywords=class_node.keywords, + body=new_body, + decorator_list=[], + ) + return ast.fix_missing_locations(new_class) + + +def strip_to_schema(source: str) -> str: + """Strip a Python source file to only schema-relevant declarations. + + Why: + Full Pydantic model source files contain imports, validators, + serializers, helper functions, type adapters, and private + attributes that are implementation internals. Agents only need + type aliases, constants, and class field definitions to understand + the YAML schema. AST-based stripping removes ~40% of tokens. + + Args: + source: Complete Python source code. + + Returns: + Stripped source containing only schema-relevant code. + """ + tree = ast.parse(source) + kept_nodes: list[ast.stmt] = [] + + for node in ast.iter_child_nodes(tree): + # Keep type aliases, module-level annotated assignments, and plain + # assignments that are not TypeAdapter instances. Skip url_dictionary + # (agents only need the SocialNetworkName type alias). + if ( + isinstance(node, ast.TypeAlias) + or (isinstance(node, ast.AnnAssign) and not is_url_dictionary(node)) + or (isinstance(node, ast.Assign) and not is_type_adapter_assignment(node)) + ): + kept_nodes.append(node) + + # Keep class definitions (stripped to fields only) + elif isinstance(node, ast.ClassDef): + kept_nodes.append(strip_class_body(node)) + + # Skip everything else: imports, functions, expressions + + # Reconstruct source from kept nodes + lines: list[str] = [] + for node in kept_nodes: + unparsed = ast.unparse(node) + lines.append(unparsed) + lines.append("") # blank line between declarations + + result = "\n".join(lines) + + # Strip redundant "The default value is `X`." from descriptions + result = re.sub( + r"\s*The default value is `[^`]*`\.?", + "", + result, + ) + + # Clean up leftover empty string concatenations (e.g., `+ ''` or `+ ""`) + result = re.sub(r"\s*\+\s*['\"]'?['\"]", "", result) + + # Strip verbose description= strings (info is covered in Important Patterns) + result = re.sub(r", description='[^']{40,}'", "", result) + result = re.sub(r', description="[^"]{40,}"', "", result) + + # Clean up multiple blank lines + result = re.sub(r"\n{3,}", "\n\n", result) + + return result.strip() + "\n" + + +def trim_sample_cv_sections(cv_yaml: str, max_entries: int = 2) -> str: + """Trim CV sections to at most max_entries entries each. + + Why: + The sample CV has 5 experience entries, 4 publications, etc. For the + skill file, showing structure matters more than volume. Trimming to + 2 entries per section saves ~2000 tokens. + + Args: + cv_yaml: YAML string containing the cv section. + max_entries: Maximum entries per section. + + Returns: + Trimmed YAML string. + """ + yaml = ruamel.yaml.YAML() + yaml.preserve_quotes = True + data = yaml.load(cv_yaml) + + sections = data.get("cv", {}).get("sections") + if sections: + for section_name in sections: + entries = sections[section_name] + if isinstance(entries, list) and len(entries) > max_entries: + sections[section_name] = entries[:max_entries] + + stream = io.StringIO() + yaml.dump(data, stream) + return stream.getvalue() + + +def build_template_context() -> dict: + """Assemble all dynamic data for the Jinja2 template. + + Returns: + Template context dictionary. + """ + # Generate sample CV + sample_cv = create_sample_cv_file(file_path=None) + sample_cv = trim_sample_cv_sections(sample_cv) + + # Generate full classic sample design + sample_classic_design = create_sample_design_file(file_path=None, theme="classic") + + # Read other theme override YAMLs (only the fields that differ from classic) + # Strip the templates: section since it's mostly the same across themes + # (noted once in the skill template instead). + other_themes_dir = models_dir / "design" / "other_themes" + theme_overrides: dict[str, str] = {} + for theme in available_themes: + if theme == "classic": + continue + yaml_path = other_themes_dir / f"{theme}.yaml" + if yaml_path.exists(): + yaml = ruamel.yaml.YAML() + data = yaml.load(yaml_path.read_text(encoding="utf-8")) + if "design" in data and "templates" in data["design"]: + del data["design"]["templates"] + stream = io.StringIO() + yaml.dump(data, stream) + theme_overrides[theme] = stream.getvalue().strip() + + # Read and strip core model source files + model_sources: dict[str, str] = {} + for name, path in MODEL_SOURCE_FILES.items(): + raw_source = path.read_text(encoding="utf-8") + model_sources[name] = strip_to_schema(raw_source) + + return { + "version": __version__, + "available_themes": available_themes, + "available_locales": available_locales, + "model_sources": model_sources, + "sample_cv": sample_cv, + "sample_classic_design": sample_classic_design, + "theme_overrides": theme_overrides, + } + + +def generate_skill_file() -> None: + """Render the Jinja2 template and write SKILL.md and docs/llms.txt.""" + env = jinja2.Environment( + loader=jinja2.FileSystemLoader(script_directory), + keep_trailing_newline=True, + trim_blocks=True, + lstrip_blocks=True, + ) + template = env.get_template(template_path.name) + context = build_template_context() + + rendered = template.render(context) + + output_path.parent.mkdir(parents=True, exist_ok=True) + output_path.write_text(rendered, encoding="utf-8") + llms_txt_path.write_text(rendered, encoding="utf-8") + + +if __name__ == "__main__": + generate_skill_file() + print("Skill generated successfully.") # NOQA: T201 diff --git a/scripts/rendercv_skill/skill_template.j2.md b/scripts/rendercv_skill/skill_template.j2.md new file mode 100644 index 00000000..1aef3ed3 --- /dev/null +++ b/scripts/rendercv_skill/skill_template.j2.md @@ -0,0 +1,297 @@ +--- +name: rendercv +description: >- + Create professional CVs and resumes with perfect typography using RenderCV + (v{{ version }}). Users write content in YAML, and RenderCV produces + publication-quality PDFs via Typst typesetting. Full control over every visual + detail: colors, fonts, margins, spacing, section title styles, entry layouts, + and more. {{ available_themes | length }} built-in themes with unlimited + customization. Any language supported ({{ available_locales | length }} built-in, or define your own). Outputs + PDF, PNG, HTML, and Markdown. Use when the user wants to create, edit, + customize, or render a CV or resume. +--- + +## Quick Start + +**Available themes:** `{{ available_themes | join("`, `") }}` +**Available locales:** `{{ available_locales | join("`, `") }}` + +These are starting points — every aspect of the design and locale can be fully customized in the YAML file. + +```bash +# Install RenderCV +uv tool install "rendercv[full]" + +# Create a starter YAML file (you can specify theme and locale) +rendercv new "John Doe" +rendercv new "John Doe" --theme moderncv --locale german + +# Render to PDF (also generates Typst, Markdown, HTML, PNG by default) +rendercv render John_Doe_CV.yaml + +# Watch mode: auto-re-render whenever the YAML file changes +rendercv render John_Doe_CV.yaml --watch + +# Render only PNG (useful for previewing or checking page count) +rendercv render John_Doe_CV.yaml --dont-generate-pdf --dont-generate-html --dont-generate-markdown + +# Override fields from the CLI without editing the YAML +rendercv render cv.yaml --cv.name "Jane Doe" --design.theme "moderncv" +``` + +## YAML Structure + +A RenderCV input has four sections. Only `cv` is required — the others have sensible defaults. + +```yaml +cv: # Your content: name, contact info, and all sections +design: # Visual styling: theme, colors, fonts, margins, spacing, layouts +locale: # Language: month names, phrases, translations +settings: # Behavior: output paths, bold keywords, current date +``` + +**Single file vs. separate files:** All four sections can live in one YAML file, or each can be a separate file. Separate files are useful for reusing the same design/locale across multiple CVs: + +```bash +# Single self-contained file (all sections in one file) +rendercv render John_Doe_CV.yaml + +# Separate files: CV content + design + locale loaded independently +rendercv render cv.yaml --design design.yaml --locale-catalog locale.yaml --settings settings.yaml +``` + +When using separate files, each file contains only its section (e.g., `design.yaml` has `design:` as the top-level key). CLI-loaded files override values in the main YAML file. + +The YAML maps directly to Pydantic models. The complete type-safe schema is provided below so you can understand every field, its type, and its default value. + +## Pydantic Schema + +The YAML input is validated against these Pydantic models. + +### Top-Level Model + +```python +{{ model_sources.rendercv_model }} +``` + +### CV Content (`cv`) + +The `cv.sections` field is a dictionary where keys are section titles (any string you want) and values are lists of entries. Each section contains entries of the same type. + +```python +{{ model_sources.cv }} +``` + +```python +{{ model_sources.social_network }} +``` + +```python +{{ model_sources.custom_connection }} +``` + +### Entry Types + +`cv.sections` is a dictionary: keys are section titles (any string), values are lists of entries. Each section must use a **single** entry type — you cannot mix different entry types within the same section. The entry type is auto-detected from the fields present in each entry. + +**Shared fields** — these are available on entry types that support dates and complex fields (ExperienceEntry, EducationEntry, NormalEntry, PublicationEntry): + +| Field | Type | Default | Notes | +|---|---|---|---| +| `date` | `str \| int \| null` | `null` | Free-form: `"2020-09"`, `"Fall 2023"`, etc. Mutually exclusive with `start_date`/`end_date`. | +| `start_date` | `str \| int \| null` | `null` | Strict format: YYYY-MM-DD, YYYY-MM, or YYYY. | +| `end_date` | `str \| int \| "present" \| null` | `null` | Same formats as `start_date`, or `"present"`. Omitting defaults to `"present"` when `start_date` is set. | +| `location` | `str \| null` | `null` | | +| `summary` | `str \| null` | `null` | | +| `highlights` | `list[str] \| null` | `null` | Bullet points. | + +**9 entry types:** + +| Entry Type | Required Fields | Optional Fields | Typical Use | +|---|---|---|---| +| **ExperienceEntry** | `company`, `position` | all shared fields | Jobs, positions | +| **EducationEntry** | `institution`, `area` | `degree` + all shared fields | Degrees, schools | +| **PublicationEntry** | `title`, `authors` | `doi`, `url`, `journal`, `summary`, `date` | Papers, articles | +| **NormalEntry** | `name` | all shared fields | Projects, awards | +| **OneLineEntry** | `label`, `details` | — | Skills, languages | +| **BulletEntry** | `bullet` | — | Simple bullet points | +| **NumberedEntry** | `number` | — | Numbered list items | +| **ReversedNumberedEntry** | `reversed_number` | — | Reverse-numbered items (5, 4, 3...) | +| **TextEntry** | *(plain string)* | — | Free-form paragraphs | + +Example: + +```yaml +cv: + sections: + experience: # list of ExperienceEntry (detected by company + position) + - company: Google + position: Engineer + start_date: 2020-01 + highlights: + - Did something impactful + skills: # list of OneLineEntry (detected by label + details) + - label: Languages + details: Python, C++ + about_me: # list of TextEntry (plain strings) + - This is a free-form paragraph about me. +``` + +Entries also accept arbitrary extra keys (silently ignored during rendering). A typo in a field name will NOT cause an error. + +### Design (`design`) + +All built-in themes share the same structure — they only differ in default values. See the sample designs below for every available field and its default. Set `design.theme` to pick a theme, then override any field. + +### Locale (`locale`) + +Built-in locales: `{{ available_locales | join("`, `") }}` + +Set `locale.language` to a built-in locale name to use it. Override any field to customize translations. Set `language` to any string and provide all translations for a fully custom locale. + +### Settings (`settings`) + +Key fields: `bold_keywords` (list of strings to auto-bold), `current_date` (override today's date), `render_command.*` (output paths, generation flags). + +## Important Patterns + +### YAML quoting + +**ALWAYS quote string values that contain a colon (`:`).** This is the most common cause of invalid YAML. Highlights, titles, summaries, and any free-form text often contain colons: + +```yaml +# WRONG — colon breaks YAML parsing: +- title: Catalytic Mechanisms: A New Approach + highlights: + - Relevant coursework: Distributed Systems, ML + +# RIGHT — wrap in double quotes: +- title: "Catalytic Mechanisms: A New Approach" + highlights: + - "Relevant coursework: Distributed Systems, ML" +``` + +Rule: if a string value contains `:`, it MUST be quoted. When in doubt, quote it. + +### Bullet characters + +The `design.highlights.bullet` field only accepts these exact characters: `●`, `•`, `◦`, `-`, `◆`, `★`, `■`, `—`, `○`. Do not use en-dash (`–`), `>`, `*`, or any other character. When in doubt, omit `bullet` to use the theme default. + +### Phone numbers + +Phone numbers MUST be in international format with country code (E.164). Never invent a phone number — only include one if the user provides it. + +```yaml +# WRONG: +phone: "(555) 123-4567" +phone: "555-123-4567" + +# RIGHT: +phone: "+15551234567" +``` + +If the user provides a local number without country code, ask which country, or omit the phone field. + +### Text formatting + +All text fields support inline Markdown: `**bold**`, `*italic*`, `[link text](url)`. Block-level Markdown (headers, lists, blockquotes, code blocks) is not supported. Raw Typst commands and math (`$$f(x)$$`) also pass through. + +### Date handling + +- `date` and `start_date`/`end_date` are mutually exclusive. If `date` is provided, `start_date` and `end_date` are ignored. +- If only `start_date` is given, `end_date` defaults to `"present"`. +- `start_date`/`end_date` require strict formats: YYYY-MM-DD, YYYY-MM, or YYYY. +- `date` is flexible: accepts any string ("Fall 2023") in addition to date formats. + +### Section titles + +- `snake_case` keys auto-capitalize: `work_experience` → "Work Experience" +- Keys with spaces or uppercase are used as-is. + +### Publication authors + +Use `*Name*` (single asterisks, italic) to highlight the CV owner in author lists. + +### Nested highlights (sub-bullets) + +```yaml +highlights: + - Main bullet point + - Sub-bullet 1 + - Sub-bullet 2 +``` + +## CLI Reference + +### `rendercv new "Full Name"` + +Generate a starter YAML file. + +| Option | Short | What it does | +|---|---|---| +| `--theme THEME` | | Theme to use (default: `classic`) | +| `--locale LOCALE` | | Locale to use (default: `english`) | +| `--create-typst-templates` | | Also create editable Typst template files for full design control | + +### `rendercv render ` + +Generate PDF, Typst, Markdown, HTML, and PNG from a YAML file. + +| Option | Short | What it does | +|---|---|---| +| `--watch` | `-w` | Re-render automatically when the YAML file changes | +| `--quiet` | `-q` | Suppress all output messages | +| `--design FILE` | `-d` | Load design section from a separate YAML file | +| `--locale-catalog FILE` | `-lc` | Load locale section from a separate YAML file | +| `--settings FILE` | `-s` | Load settings section from a separate YAML file | +| `--output-folder DIR` | `-o` | Custom output directory | + +Per-format controls: `--{format}-path PATH` sets custom output path, `--dont-generate-{format}` skips generation. Formats: `pdf`, `typst`, `markdown`, `html`, `png`. + +**Override any YAML field from the CLI** using dot notation (overrides without editing the file): + +```bash +rendercv render CV.yaml --cv.name "Jane Doe" --design.theme "moderncv" +rendercv render CV.yaml --cv.sections.education.0.institution "MIT" +``` + +### `rendercv create-theme "theme-name"` + +Scaffold a custom theme directory with editable Typst templates for complete design control. + +## JSON Schema + +For YAML editor autocompletion and validation: + +```yaml +# yaml-language-server: $schema=https://raw.githubusercontent.com/rendercv/rendercv/refs/tags/v{{ version }}/schema.json +``` + +## Complete Example + +### Sample CV + +```yaml +{{ sample_cv }} +``` + +### Sample Design (classic — complete reference) + +This shows every available design field with its default value. All themes share the same structure. + +```yaml +{{ sample_classic_design }} +``` + +### Other Theme Overrides + +Other themes only override specific fields from the classic defaults above. To use a theme, set `design.theme` and optionally override any field. Each theme also customizes `design.templates` (entry layout patterns) — see the classic sample above for the full template structure. The override YAMLs below omit templates for brevity. + +{% for theme, yaml in theme_overrides.items() %} +#### {{ theme }} + +```yaml +{{ yaml }} +``` + +{% endfor %} diff --git a/skills/rendercv/SKILL.md b/skills/rendercv/SKILL.md new file mode 100644 index 00000000..b436ae3b --- /dev/null +++ b/skills/rendercv/SKILL.md @@ -0,0 +1,805 @@ +--- +name: rendercv +description: >- + Create professional CVs and resumes with perfect typography using RenderCV + (v2.8). Users write content in YAML, and RenderCV produces + publication-quality PDFs via Typst typesetting. Full control over every visual + detail: colors, fonts, margins, spacing, section title styles, entry layouts, + and more. 6 built-in themes with unlimited + customization. Any language supported (20 built-in, or define your own). Outputs + PDF, PNG, HTML, and Markdown. Use when the user wants to create, edit, + customize, or render a CV or resume. +--- + +## Quick Start + +**Available themes:** `classic`, `engineeringclassic`, `engineeringresumes`, `harvard`, `moderncv`, `sb2nov` +**Available locales:** `english`, `arabic`, `danish`, `dutch`, `french`, `german`, `hebrew`, `hindi`, `indonesian`, `italian`, `japanese`, `korean`, `mandarin_chinese`, `norwegian_bokmål`, `norwegian_nynorsk`, `persian`, `portuguese`, `russian`, `spanish`, `turkish` + +These are starting points — every aspect of the design and locale can be fully customized in the YAML file. + +```bash +# Install RenderCV +uv tool install "rendercv[full]" + +# Create a starter YAML file (you can specify theme and locale) +rendercv new "John Doe" +rendercv new "John Doe" --theme moderncv --locale german + +# Render to PDF (also generates Typst, Markdown, HTML, PNG by default) +rendercv render John_Doe_CV.yaml + +# Watch mode: auto-re-render whenever the YAML file changes +rendercv render John_Doe_CV.yaml --watch + +# Render only PNG (useful for previewing or checking page count) +rendercv render John_Doe_CV.yaml --dont-generate-pdf --dont-generate-html --dont-generate-markdown + +# Override fields from the CLI without editing the YAML +rendercv render cv.yaml --cv.name "Jane Doe" --design.theme "moderncv" +``` + +## YAML Structure + +A RenderCV input has four sections. Only `cv` is required — the others have sensible defaults. + +```yaml +cv: # Your content: name, contact info, and all sections +design: # Visual styling: theme, colors, fonts, margins, spacing, layouts +locale: # Language: month names, phrases, translations +settings: # Behavior: output paths, bold keywords, current date +``` + +**Single file vs. separate files:** All four sections can live in one YAML file, or each can be a separate file. Separate files are useful for reusing the same design/locale across multiple CVs: + +```bash +# Single self-contained file (all sections in one file) +rendercv render John_Doe_CV.yaml + +# Separate files: CV content + design + locale loaded independently +rendercv render cv.yaml --design design.yaml --locale-catalog locale.yaml --settings settings.yaml +``` + +When using separate files, each file contains only its section (e.g., `design.yaml` has `design:` as the top-level key). CLI-loaded files override values in the main YAML file. + +The YAML maps directly to Pydantic models. The complete type-safe schema is provided below so you can understand every field, its type, and its default value. + +## Pydantic Schema + +The YAML input is validated against these Pydantic models. + +### Top-Level Model + +```python +class RenderCVModel(BaseModelWithoutExtraKeys): + cv: Cv = pydantic.Field(default_factory=Cv, title='CV', description='The content of the CV.') + design: Design = pydantic.Field(default_factory=ClassicTheme, title='Design') + locale: Locale = pydantic.Field(default_factory=EnglishLocale, title='Locale Catalog') + settings: Settings = pydantic.Field(default_factory=Settings, title='RenderCV Settings', description='The settings of the RenderCV.') + +``` + +### CV Content (`cv`) + +The `cv.sections` field is a dictionary where keys are section titles (any string you want) and values are lists of entries. Each section contains entries of the same type. + +```python +class Cv(BaseModelWithoutExtraKeys): + name: str | None = pydantic.Field(default=None, examples=['John Doe', 'Jane Smith']) + headline: str | None = pydantic.Field(default=None, examples=['Software Engineer', 'Data Scientist', 'Product Manager']) + location: str | None = pydantic.Field(default=None, examples=['New York, NY', 'London, UK', 'Istanbul, Türkiye']) + email: pydantic.EmailStr | list[pydantic.EmailStr] | None = pydantic.Field(default=None, examples=['john.doe@example.com', ['john.doe.1@example.com', 'john.doe.2@example.com']]) + photo: ExistingPathRelativeToInput | pydantic.HttpUrl | None = pydantic.Field(default=None, union_mode='left_to_right', examples=['photo.jpg', 'images/profile.png', 'https://example.com/photo.jpg']) + phone: pydantic_phone_numbers.PhoneNumber | list[pydantic_phone_numbers.PhoneNumber] | None = pydantic.Field(default=None, examples=['+1-234-567-8900', ['+1-234-567-8900', '+44 20 1234 5678']]) + website: pydantic.HttpUrl | list[pydantic.HttpUrl] | None = pydantic.Field(default=None, examples=['https://johndoe.com', ['https://johndoe.com', 'https://www.janesmith.dev']]) + social_networks: list[SocialNetwork] | None = pydantic.Field(default=None) + custom_connections: list[CustomConnection] | None = pydantic.Field(default=None, examples=[[{'placeholder': 'Book a call', 'url': 'https://cal.com/johndoe', 'fontawesome_icon': 'calendar-days'}]]) + sections: dict[str, Section] | None = pydantic.Field(default=None, examples=[{'Experience': '...', 'Education': '...', 'Projects': '...', 'Skills': '...'}]) + +``` + +```python +type SocialNetworkName = Literal['LinkedIn', 'GitHub', 'GitLab', 'IMDB', 'Instagram', 'ORCID', 'Mastodon', 'StackOverflow', 'ResearchGate', 'YouTube', 'Google Scholar', 'Telegram', 'WhatsApp', 'Leetcode', 'X', 'Bluesky', 'Reddit'] + +available_social_networks = get_args(SocialNetworkName.__value__) + +class SocialNetwork(BaseModelWithoutExtraKeys): + network: SocialNetworkName = pydantic.Field() + username: str = pydantic.Field(examples=['john_doe', '@johndoe@mastodon.social', '12345/john-doe']) + +``` + +```python +class CustomConnection(BaseModelWithoutExtraKeys): + fontawesome_icon: str + placeholder: str + url: pydantic.HttpUrl | None + +``` + +### Entry Types + +`cv.sections` is a dictionary: keys are section titles (any string), values are lists of entries. Each section must use a **single** entry type — you cannot mix different entry types within the same section. The entry type is auto-detected from the fields present in each entry. + +**Shared fields** — these are available on entry types that support dates and complex fields (ExperienceEntry, EducationEntry, NormalEntry, PublicationEntry): + +| Field | Type | Default | Notes | +|---|---|---|---| +| `date` | `str \| int \| null` | `null` | Free-form: `"2020-09"`, `"Fall 2023"`, etc. Mutually exclusive with `start_date`/`end_date`. | +| `start_date` | `str \| int \| null` | `null` | Strict format: YYYY-MM-DD, YYYY-MM, or YYYY. | +| `end_date` | `str \| int \| "present" \| null` | `null` | Same formats as `start_date`, or `"present"`. Omitting defaults to `"present"` when `start_date` is set. | +| `location` | `str \| null` | `null` | | +| `summary` | `str \| null` | `null` | | +| `highlights` | `list[str] \| null` | `null` | Bullet points. | + +**9 entry types:** + +| Entry Type | Required Fields | Optional Fields | Typical Use | +|---|---|---|---| +| **ExperienceEntry** | `company`, `position` | all shared fields | Jobs, positions | +| **EducationEntry** | `institution`, `area` | `degree` + all shared fields | Degrees, schools | +| **PublicationEntry** | `title`, `authors` | `doi`, `url`, `journal`, `summary`, `date` | Papers, articles | +| **NormalEntry** | `name` | all shared fields | Projects, awards | +| **OneLineEntry** | `label`, `details` | — | Skills, languages | +| **BulletEntry** | `bullet` | — | Simple bullet points | +| **NumberedEntry** | `number` | — | Numbered list items | +| **ReversedNumberedEntry** | `reversed_number` | — | Reverse-numbered items (5, 4, 3...) | +| **TextEntry** | *(plain string)* | — | Free-form paragraphs | + +Example: + +```yaml +cv: + sections: + experience: # list of ExperienceEntry (detected by company + position) + - company: Google + position: Engineer + start_date: 2020-01 + highlights: + - Did something impactful + skills: # list of OneLineEntry (detected by label + details) + - label: Languages + details: Python, C++ + about_me: # list of TextEntry (plain strings) + - This is a free-form paragraph about me. +``` + +Entries also accept arbitrary extra keys (silently ignored during rendering). A typo in a field name will NOT cause an error. + +### Design (`design`) + +All built-in themes share the same structure — they only differ in default values. See the sample designs below for every available field and its default. Set `design.theme` to pick a theme, then override any field. + +### Locale (`locale`) + +Built-in locales: `english`, `arabic`, `danish`, `dutch`, `french`, `german`, `hebrew`, `hindi`, `indonesian`, `italian`, `japanese`, `korean`, `mandarin_chinese`, `norwegian_bokmål`, `norwegian_nynorsk`, `persian`, `portuguese`, `russian`, `spanish`, `turkish` + +Set `locale.language` to a built-in locale name to use it. Override any field to customize translations. Set `language` to any string and provide all translations for a fully custom locale. + +### Settings (`settings`) + +Key fields: `bold_keywords` (list of strings to auto-bold), `current_date` (override today's date), `render_command.*` (output paths, generation flags). + +## Important Patterns + +### YAML quoting + +**ALWAYS quote string values that contain a colon (`:`).** This is the most common cause of invalid YAML. Highlights, titles, summaries, and any free-form text often contain colons: + +```yaml +# WRONG — colon breaks YAML parsing: +- title: Catalytic Mechanisms: A New Approach + highlights: + - Relevant coursework: Distributed Systems, ML + +# RIGHT — wrap in double quotes: +- title: "Catalytic Mechanisms: A New Approach" + highlights: + - "Relevant coursework: Distributed Systems, ML" +``` + +Rule: if a string value contains `:`, it MUST be quoted. When in doubt, quote it. + +### Bullet characters + +The `design.highlights.bullet` field only accepts these exact characters: `●`, `•`, `◦`, `-`, `◆`, `★`, `■`, `—`, `○`. Do not use en-dash (`–`), `>`, `*`, or any other character. When in doubt, omit `bullet` to use the theme default. + +### Phone numbers + +Phone numbers MUST be in international format with country code (E.164). Never invent a phone number — only include one if the user provides it. + +```yaml +# WRONG: +phone: "(555) 123-4567" +phone: "555-123-4567" + +# RIGHT: +phone: "+15551234567" +``` + +If the user provides a local number without country code, ask which country, or omit the phone field. + +### Text formatting + +All text fields support inline Markdown: `**bold**`, `*italic*`, `[link text](url)`. Block-level Markdown (headers, lists, blockquotes, code blocks) is not supported. Raw Typst commands and math (`$$f(x)$$`) also pass through. + +### Date handling + +- `date` and `start_date`/`end_date` are mutually exclusive. If `date` is provided, `start_date` and `end_date` are ignored. +- If only `start_date` is given, `end_date` defaults to `"present"`. +- `start_date`/`end_date` require strict formats: YYYY-MM-DD, YYYY-MM, or YYYY. +- `date` is flexible: accepts any string ("Fall 2023") in addition to date formats. + +### Section titles + +- `snake_case` keys auto-capitalize: `work_experience` → "Work Experience" +- Keys with spaces or uppercase are used as-is. + +### Publication authors + +Use `*Name*` (single asterisks, italic) to highlight the CV owner in author lists. + +### Nested highlights (sub-bullets) + +```yaml +highlights: + - Main bullet point + - Sub-bullet 1 + - Sub-bullet 2 +``` + +## CLI Reference + +### `rendercv new "Full Name"` + +Generate a starter YAML file. + +| Option | Short | What it does | +|---|---|---| +| `--theme THEME` | | Theme to use (default: `classic`) | +| `--locale LOCALE` | | Locale to use (default: `english`) | +| `--create-typst-templates` | | Also create editable Typst template files for full design control | + +### `rendercv render ` + +Generate PDF, Typst, Markdown, HTML, and PNG from a YAML file. + +| Option | Short | What it does | +|---|---|---| +| `--watch` | `-w` | Re-render automatically when the YAML file changes | +| `--quiet` | `-q` | Suppress all output messages | +| `--design FILE` | `-d` | Load design section from a separate YAML file | +| `--locale-catalog FILE` | `-lc` | Load locale section from a separate YAML file | +| `--settings FILE` | `-s` | Load settings section from a separate YAML file | +| `--output-folder DIR` | `-o` | Custom output directory | + +Per-format controls: `--{format}-path PATH` sets custom output path, `--dont-generate-{format}` skips generation. Formats: `pdf`, `typst`, `markdown`, `html`, `png`. + +**Override any YAML field from the CLI** using dot notation (overrides without editing the file): + +```bash +rendercv render CV.yaml --cv.name "Jane Doe" --design.theme "moderncv" +rendercv render CV.yaml --cv.sections.education.0.institution "MIT" +``` + +### `rendercv create-theme "theme-name"` + +Scaffold a custom theme directory with editable Typst templates for complete design control. + +## JSON Schema + +For YAML editor autocompletion and validation: + +```yaml +# yaml-language-server: $schema=https://raw.githubusercontent.com/rendercv/rendercv/refs/tags/v2.8/schema.json +``` + +## Complete Example + +### Sample CV + +```yaml +cv: + name: John Doe + headline: + location: San Francisco, CA + email: john.doe@email.com + photo: + phone: + website: https://rendercv.com/ + social_networks: + - network: LinkedIn + username: rendercv + - network: GitHub + username: rendercv + custom_connections: + sections: + Welcome to RenderCV: + - RenderCV reads a CV written in a YAML file, and generates a PDF with + professional typography. + - Each section title is arbitrary. + education: + - institution: Princeton University + area: Computer Science + degree: PhD + date: + start_date: 2018-09 + end_date: 2023-05 + location: Princeton, NJ + summary: + highlights: + - 'Thesis: Efficient Neural Architecture Search for Resource-Constrained Deployment' + - 'Advisor: Prof. Sanjeev Arora' + - NSF Graduate Research Fellowship, Siebel Scholar (Class of 2022) + - institution: Boğaziçi University + area: Computer Engineering + degree: BS + date: + start_date: 2014-09 + end_date: 2018-06 + location: Istanbul, Türkiye + summary: + highlights: + - 'GPA: 3.97/4.00, Valedictorian' + - Fulbright Scholarship recipient for Graduate Studies + experience: + - company: Nexus AI + position: Co-Founder & CTO + date: + start_date: 2023-06 + end_date: present + location: San Francisco, CA + summary: + highlights: + - Built foundation model infrastructure serving 2M+ monthly API requests + with 99.97% uptime + - Raised $18M Series A led by Sequoia Capital, with participation from + a16z and Founders Fund + - Scaled engineering team from 3 to 28 across ML research, platform, and + applied AI divisions + - Developed proprietary inference optimization reducing latency by 73% + compared to baseline + - company: NVIDIA Research + position: Research Intern + date: + start_date: 2022-05 + end_date: 2022-08 + location: Santa Clara, CA + summary: + highlights: + - Designed sparse attention mechanism reducing transformer memory + footprint by 4.2x + - Co-authored paper accepted at NeurIPS 2022 (spotlight presentation, top + 5% of submissions) + projects: + - name: '[FlashInfer](https://github.com/)' + date: + start_date: 2023-01 + end_date: present + location: + summary: Open-source library for high-performance LLM inference kernels + highlights: + - Achieved 2.8x speedup over baseline attention implementations on A100 + GPUs + - Adopted by 3 major AI labs, 8,500+ GitHub stars, 200+ contributors + - name: '[NeuralPrune](https://github.com/)' + date: '2021' + start_date: + end_date: + location: + summary: Automated neural network pruning toolkit with differentiable + masks + highlights: + - Reduced model size by 90% with less than 1% accuracy degradation on + ImageNet + - Featured in PyTorch ecosystem tools, 4,200+ GitHub stars + publications: + - title: 'Sparse Mixture-of-Experts at Scale: Efficient Routing for Trillion-Parameter + Models' + authors: + - '*John Doe*' + - Sarah Williams + - David Park + summary: + doi: 10.1234/neurips.2023.1234 + url: + journal: NeurIPS 2023 + date: 2023-07 + - title: Neural Architecture Search via Differentiable Pruning + authors: + - James Liu + - '*John Doe*' + summary: + doi: 10.1234/neurips.2022.5678 + url: + journal: NeurIPS 2022, Spotlight + date: 2022-12 + selected_honors: + - bullet: MIT Technology Review 35 Under 35 Innovators (2024) + - bullet: Forbes 30 Under 30 in Enterprise Technology (2024) + skills: + - label: Languages + details: Python, C++, CUDA, Rust, Julia + - label: ML Frameworks + details: PyTorch, JAX, TensorFlow, Triton, ONNX + patents: + - number: Adaptive Quantization for Neural Network Inference on Edge Devices + (US Patent 11,234,567) + - number: Dynamic Sparsity Patterns for Efficient Transformer Attention (US + Patent 11,345,678) + invited_talks: + - reversed_number: Scaling Laws for Efficient Inference — Stanford HAI + Symposium (2024) + - reversed_number: Building AI Infrastructure for the Next Decade — + TechCrunch Disrupt (2024) + +``` + +### Sample Design (classic — complete reference) + +This shows every available design field with its default value. All themes share the same structure. + +```yaml +design: + theme: classic + page: + size: us-letter + top_margin: 0.7in + bottom_margin: 0.7in + left_margin: 0.7in + right_margin: 0.7in + show_footer: true + show_top_note: true + colors: + body: rgb(0, 0, 0) + name: rgb(0, 79, 144) + headline: rgb(0, 79, 144) + connections: rgb(0, 79, 144) + section_titles: rgb(0, 79, 144) + links: rgb(0, 79, 144) + footer: rgb(128, 128, 128) + top_note: rgb(128, 128, 128) + typography: + line_spacing: 0.6em + alignment: justified + date_and_location_column_alignment: right + font_family: + body: Source Sans 3 + name: Source Sans 3 + headline: Source Sans 3 + connections: Source Sans 3 + section_titles: Source Sans 3 + font_size: + body: 10pt + name: 30pt + headline: 10pt + connections: 10pt + section_titles: 1.4em + small_caps: + name: false + headline: false + connections: false + section_titles: false + bold: + name: true + headline: false + connections: false + section_titles: true + links: + underline: false + show_external_link_icon: false + header: + alignment: center + photo_width: 3.5cm + photo_position: left + photo_space_left: 0.4cm + photo_space_right: 0.4cm + space_below_name: 0.7cm + space_below_headline: 0.7cm + space_below_connections: 0.7cm + connections: + phone_number_format: national + hyperlink: true + show_icons: true + display_urls_instead_of_usernames: false + separator: '' + space_between_connections: 0.5cm + section_titles: + type: with_partial_line + line_thickness: 0.5pt + space_above: 0.5cm + space_below: 0.3cm + sections: + allow_page_break: true + space_between_regular_entries: 1.2em + space_between_text_based_entries: 0.3em + show_time_spans_in: + - experience + entries: + date_and_location_width: 4.15cm + side_space: 0.2cm + space_between_columns: 0.1cm + allow_page_break: false + short_second_row: true + degree_width: 1cm + summary: + space_above: 0cm + space_left: 0cm + highlights: + bullet: • + nested_bullet: • + space_left: 0.15cm + space_above: 0cm + space_between_items: 0cm + space_between_bullet_and_text: 0.5em + templates: + footer: '*NAME -- PAGE_NUMBER/TOTAL_PAGES*' + top_note: '*LAST_UPDATED CURRENT_DATE*' + single_date: MONTH_ABBREVIATION YEAR + date_range: START_DATE – END_DATE + time_span: HOW_MANY_YEARS YEARS HOW_MANY_MONTHS MONTHS + one_line_entry: + main_column: '**LABEL:** DETAILS' + education_entry: + main_column: |- + **INSTITUTION**, AREA + SUMMARY + HIGHLIGHTS + degree_column: '**DEGREE**' + date_and_location_column: |- + LOCATION + DATE + normal_entry: + main_column: |- + **NAME** + SUMMARY + HIGHLIGHTS + date_and_location_column: |- + LOCATION + DATE + experience_entry: + main_column: |- + **COMPANY**, POSITION + SUMMARY + HIGHLIGHTS + date_and_location_column: |- + LOCATION + DATE + publication_entry: + main_column: |- + **TITLE** + SUMMARY + AUTHORS + URL (JOURNAL) + date_and_location_column: DATE + +``` + +### Other Theme Overrides + +Other themes only override specific fields from the classic defaults above. To use a theme, set `design.theme` and optionally override any field. Each theme also customizes `design.templates` (entry layout patterns) — see the classic sample above for the full template structure. The override YAMLs below omit templates for brevity. + +#### engineeringclassic + +```yaml +# yaml-language-server: $schema=../../../../../../schema.json +design: + theme: engineeringclassic + typography: + font_family: + body: Raleway + name: Raleway + headline: Raleway + connections: Raleway + section_titles: Raleway + bold: + name: false + section_titles: false + header: + alignment: left + links: + show_external_link_icon: false + section_titles: + type: with_full_line + sections: + show_time_spans_in: [] + entries: + short_second_row: false + summary: + space_above: 0.12cm + highlights: + space_left: 0cm + space_above: 0.12cm + space_between_items: 0.12cm +``` + +#### engineeringresumes + +```yaml +# yaml-language-server: $schema=../../../../../../schema.json +design: + theme: engineeringresumes + page: + show_footer: false + typography: + font_family: + body: XCharter + name: XCharter + headline: XCharter + connections: XCharter + section_titles: XCharter + font_size: + name: 25pt + section_titles: 1.2em + bold: + name: false + header: + connections: + separator: '|' + show_icons: false + display_urls_instead_of_usernames: true + colors: + name: rgb(0,0,0) + connections: rgb(0,0,0) + headline: rgb(0,0,0) + section_titles: rgb(0,0,0) + links: rgb(0,0,0) + links: + underline: true + show_external_link_icon: false + section_titles: + type: with_full_line + space_above: 0.5cm + space_below: 0.3cm + sections: + space_between_regular_entries: 0.42cm + space_between_text_based_entries: 0.15cm + show_time_spans_in: [] + entries: + short_second_row: false + summary: + space_above: 0.08cm + side_space: 0cm + highlights: + bullet: ● + nested_bullet: ● + space_left: 0cm + space_above: 0.08cm + space_between_items: 0.08cm + space_between_bullet_and_text: 0.3em +``` + +#### harvard + +```yaml +# yaml-language-server: $schema=../../../../../../schema.json +design: + theme: harvard + page: + top_margin: 0.5in + bottom_margin: 0.5in + left_margin: 0.5in + right_margin: 0.5in + show_top_note: false + colors: + name: rgb(0,0,0) + headline: rgb(0,0,0) + connections: rgb(0,0,0) + section_titles: rgb(0,0,0) + links: rgb(0,0,0) + typography: + font_family: + body: XCharter + name: XCharter + headline: XCharter + connections: XCharter + section_titles: XCharter + font_size: + name: 25pt + connections: 9pt + section_titles: 1.3em + header: + space_below_name: 0.5cm + space_below_headline: 0.5cm + space_below_connections: 0.5cm + connections: + show_icons: false + separator: • + space_between_connections: 0.4cm + section_titles: + type: centered_with_centered_partial_line + space_below: 0.2cm + sections: + space_between_regular_entries: 1em + show_time_spans_in: [] + entries: + short_second_row: false +``` + +#### moderncv + +```yaml +# yaml-language-server: $schema=../../../../../../schema.json +design: + theme: moderncv + typography: + line_spacing: 0.6em + font_family: + body: Fontin + name: Fontin + headline: Fontin + connections: Fontin + section_titles: Fontin + font_size: + name: 25pt + section_titles: 1.4em + bold: + name: false + section_titles: false + header: + alignment: left + photo_width: 4.15cm + photo_space_left: 0cm + photo_space_right: 0.3cm + links: + underline: true + show_external_link_icon: false + section_titles: + type: moderncv + space_above: 0.55cm + space_below: 0.3cm + line_thickness: 0.15cm + sections: + show_time_spans_in: [] + entries: + short_second_row: false + side_space: 0cm + space_between_columns: 0.3cm + summary: + space_above: 0.1cm + highlights: + space_left: 0cm + space_above: 0.15cm + space_between_items: 0.1cm + space_between_bullet_and_text: 0.3em +``` + +#### sb2nov + +```yaml +# yaml-language-server: $schema=../../../../../../schema.json +design: + theme: sb2nov + typography: + font_family: + body: New Computer Modern + name: New Computer Modern + headline: New Computer Modern + connections: New Computer Modern + section_titles: New Computer Modern + colors: + name: rgb(0,0,0) + connections: rgb(0,0,0) + section_titles: rgb(0,0,0) + headline: rgb(0,0,0) + links: rgb(0,0,0) + links: + underline: true + show_external_link_icon: false + section_titles: + type: with_full_line + sections: + show_time_spans_in: [] + header: + connections: + hyperlink: true + show_icons: false + display_urls_instead_of_usernames: true + separator: • + entries: + short_second_row: false + highlights: + bullet: ◦ + nested_bullet: ◦ +``` + diff --git a/tests/renderer/test_pdf_png.py b/tests/renderer/test_pdf_png.py index 383a6942..c27d7201 100644 --- a/tests/renderer/test_pdf_png.py +++ b/tests/renderer/test_pdf_png.py @@ -88,12 +88,12 @@ class TestGetPackagePath: def test_raises_error_when_version_missing_from_typst_toml(self): get_package_path.cache_clear() - toml_without_version = "[package]\nname = \"rendercv\"\n" + toml_without_version = '[package]\nname = "rendercv"\n' with ( patch("pathlib.Path.read_text", return_value=toml_without_version), pytest.raises( RenderCVInternalError, - match="Could not find version in bundled typst.toml", + match=r"Could not find version in bundled typst\.toml", ), ): get_package_path() diff --git a/tests/schema/test_sample_generator.py b/tests/schema/test_sample_generator.py index 7f831ebd..4a7e462a 100644 --- a/tests/schema/test_sample_generator.py +++ b/tests/schema/test_sample_generator.py @@ -165,7 +165,9 @@ class TestCreateSampleSettingsFile: assert list(data.keys()) == ["settings"] def test_omits_specified_fields(self): - result = create_sample_settings_file(omitted_fields=["render_command"]) + result = create_sample_settings_file( + file_path=None, omitted_fields=["render_command"] + ) yaml_object = ruamel.yaml.YAML() data = yaml_object.load(result) diff --git a/tests/test_generated_files.py b/tests/test_generated_files.py new file mode 100644 index 00000000..b35d64e8 --- /dev/null +++ b/tests/test_generated_files.py @@ -0,0 +1,87 @@ +"""Tests that auto-generated files are up to date with the source code. + +Why: + Several files in the repository (schema.json, example YAMLs, SKILL.md, + llms.txt) are derived from source code. These tests fail when the committed + files diverge from what the generation code produces, catching staleness on + every PR instead of silently regenerating in CI. +""" + +import pathlib +import subprocess + +import pytest + +from rendercv.schema.models.design.built_in_design import available_themes +from rendercv.schema.sample_generator import create_sample_yaml_input_file + +repository_root = pathlib.Path(__file__).parent.parent + + +def run_just(recipe: str) -> None: + """Run a just recipe and raise on failure.""" + subprocess.run( + ["just", recipe], + cwd=repository_root, + check=True, + capture_output=True, + ) + + +def test_schema_json_is_up_to_date() -> None: + schema_path = repository_root / "schema.json" + before = schema_path.read_text(encoding="utf-8") + + run_just("update-schema") + + after = schema_path.read_text(encoding="utf-8") + # Restore original content + schema_path.write_text(before, encoding="utf-8") + + assert before == after, ( + "schema.json is stale. Run `just update-schema` to regenerate." + ) + + +@pytest.mark.parametrize("theme", available_themes) +def test_example_yaml_is_up_to_date(theme: str) -> None: + yaml_path = ( + repository_root / "examples" / f"John_Doe_{theme.capitalize()}Theme_CV.yaml" + ) + before = yaml_path.read_text(encoding="utf-8") + + expected = create_sample_yaml_input_file( + file_path=None, + name="John Doe", + theme=theme, + locale="english", + ) + + assert before == expected, ( + f"examples/John_Doe_{theme.capitalize()}Theme_CV.yaml is stale. " + "Run `just update-examples` to regenerate." + ) + + +def test_skill_md_is_up_to_date() -> None: + skill_path = repository_root / "skills" / "rendercv" / "SKILL.md" + before = skill_path.read_text(encoding="utf-8") + + llms_txt_path = repository_root / "docs" / "llms.txt" + llms_before = llms_txt_path.read_text(encoding="utf-8") + + run_just("update-skill") + + skill_after = skill_path.read_text(encoding="utf-8") + llms_after = llms_txt_path.read_text(encoding="utf-8") + + # Restore original content + skill_path.write_text(before, encoding="utf-8") + llms_txt_path.write_text(llms_before, encoding="utf-8") + + assert before == skill_after, ( + "skills/rendercv/SKILL.md is stale. Run `just update-skill` to regenerate." + ) + assert llms_before == llms_after, ( + "docs/llms.txt is stale. Run `just update-skill` to regenerate." + ) diff --git a/uv.lock b/uv.lock index c172fb35..77d1fc30 100644 --- a/uv.lock +++ b/uv.lock @@ -1189,7 +1189,7 @@ wheels = [ [[package]] name = "rendercv" -version = "2.7" +version = "2.8" source = { editable = "." } dependencies = [ { name = "jinja2" },