feat: add Carbone tags help - view in-app modal and download as .docx

This commit is contained in:
root
2026-07-16 09:19:06 +00:00
parent 49a6081c92
commit 6fa8c58179
5 changed files with 522 additions and 0 deletions

View File

@@ -473,6 +473,128 @@ router.get('/master-template', async (req, res) => {
}
});
// GET /api/carbone/help-docx - Download tags reference as .docx
router.get('/help-docx', (req, res) => {
const helpPath = path.join(TEMPLATES_DIR, 'HELP_TAGS_REFERENCE.docx');
if (fs.existsSync(helpPath)) {
res.download(helpPath, 'Carbone_Tags_Reference.docx');
} else {
res.status(404).json({ error: 'Help file not found' });
}
});
// GET /api/carbone/help - Get tags reference as JSON (for in-app viewing)
router.get('/help', (req, res) => {
res.json({
sections: [
{
title: '1. Personal Details',
type: 'simple',
tags: [
{ tag: '{d.fullName}', desc: 'Full name (first + last)' },
{ tag: '{d.firstName}', desc: 'First name only' },
{ tag: '{d.lastName}', desc: 'Last name only' },
{ tag: '{d.email}', desc: 'Email address' },
{ tag: '{d.phone}', desc: 'Phone number' },
{ tag: '{d.address}', desc: 'Physical address' },
{ tag: '{d.linkedin}', desc: 'LinkedIn profile URL' },
{ tag: '{d.github}', desc: 'GitHub profile URL' },
{ tag: '{d.website}', desc: 'Personal website' },
{ tag: '{d.summary}', desc: 'Professional summary paragraph' },
{ tag: '{d.generationDate}', desc: 'Date the CV was generated' }
]
},
{
title: '2. Work Experience (Loop)',
type: 'loop',
note: 'Carbone auto-detects loops from [i]. Section repeats for each job.',
tags: [
{ tag: '{d.experience[i].position}', desc: 'Job title' },
{ tag: '{d.experience[i].company}', desc: 'Company name' },
{ tag: '{d.experience[i].location}', desc: 'Work location' },
{ tag: '{d.experience[i].startDate}', desc: 'Start date (Mon YYYY)' },
{ tag: '{d.experience[i].endDate}', desc: 'End date or "Present"' },
{ tag: '{d.experience[i].duration}', desc: 'Calculated duration (e.g., 4.5 years)' },
{ tag: '{d.experience[i].description}', desc: 'Job description' },
{ tag: '{d.experience[i].skillsUsed}', desc: 'Comma-separated skills used' },
{ tag: '{d.experience[i].achievements[j]}', desc: 'Achievement bullet (sub-loop)' }
]
},
{
title: '3. Skills',
type: 'loop',
tags: [
{ tag: '{d.skills[i].name}', desc: 'Skill name' },
{ tag: '{d.skills[i].category}', desc: 'Skill category' },
{ tag: '{d.skills[i].proficiency}', desc: 'Proficiency level' },
{ tag: '{d.skills[i].years}', desc: 'Years of experience (dynamic)' },
{ tag: '{d.skillsByCategory[i].category}', desc: 'Category name (grouped)' },
{ tag: '{d.skillsByCategory[i].skills[j].name}', desc: 'Skill in category (grouped)' },
{ tag: '{d.skillsByCategory[i].skills[j].years}', desc: 'Years in category (grouped)' }
]
},
{
title: '4. Education (Loop)',
type: 'loop',
tags: [
{ tag: '{d.education[i].degree}', desc: 'Degree name' },
{ tag: '{d.education[i].field}', desc: 'Field of study' },
{ tag: '{d.education[i].institution}', desc: 'University/institution' },
{ tag: '{d.education[i].startDate}', desc: 'Start date' },
{ tag: '{d.education[i].endDate}', desc: 'End date' },
{ tag: '{d.education[i].grade}', desc: 'Grade/result' }
]
},
{
title: '5. Certifications (Loop)',
type: 'loop',
tags: [
{ tag: '{d.certifications[i].name}', desc: 'Certification name' },
{ tag: '{d.certifications[i].issuer}', desc: 'Issuing organization' },
{ tag: '{d.certifications[i].date}', desc: 'Issue date' },
{ tag: '{d.certifications[i].expiryDate}', desc: 'Expiry date (if any)' }
]
},
{
title: '6. Conditionals (Show/Hide)',
type: 'simple',
note: 'Show content only when a field has a value (or is empty).',
tags: [
{ tag: '{d.field:showBegin}...{d.field:showEnd}', desc: 'Show if NOT empty' },
{ tag: '{d.field:hideBegin}...{d.field:hideEnd}', desc: 'Show if empty' },
{ tag: '{d.field:ifEM(replacement text)}', desc: 'Show text if field is empty' },
{ tag: '{d.field:ifNEM(replacement text)}', desc: 'Show text if field is not empty' },
{ tag: '{d.field:ifEQ(value):showBegin}...{d.field:showEnd}', desc: 'Show if equals value' },
{ tag: '{d.field:ifContain(text):showBegin}...{d.field:showEnd}', desc: 'Show if contains text' },
{ tag: '{d.field:ifGT(5):showBegin}...{d.field:showEnd}', desc: 'Show if greater than value' }
]
},
{
title: '7. Formatting',
type: 'simple',
tags: [
{ tag: '{d.field:formatD(YYYY-MM-DD)}', desc: 'Format date: 2022-01-01' },
{ tag: '{d.field:formatD(MMM YYYY)}', desc: 'Format date: Jan 2022' },
{ tag: '{d.field:formatN(0.0)}', desc: 'Format number with 1 decimal' },
{ tag: '{d.field:upperCase}', desc: 'Convert to UPPERCASE' },
{ tag: '{d.field:lowerCase}', desc: 'Convert to lowercase' },
{ tag: '{d.field:ucWords}', desc: 'Capitalize Each Word' },
{ tag: '{d.field:substr(0, 100)}', desc: 'First 100 characters' }
]
},
{
title: '8. Array Operations',
type: 'simple',
tags: [
{ tag: '{d.skills:arrayJoin(, )}', desc: 'Join all items with separator' },
{ tag: '{d.skills:arrayMap(name):arrayJoin(, )}', desc: 'Extract field and join' },
{ tag: '{d.experience:count}', desc: 'Count items in array' }
]
}
]
});
});
// POST /api/carbone/templates/upload - Upload a custom .docx template
router.post('/templates/upload', upload.single('template'), (req, res) => {
if (!req.file) {

Binary file not shown.

View File

@@ -0,0 +1,364 @@
# Carbone Template Tags Reference
This document lists all available Carbone tags you can use in your CV templates (.docx files).
You can use these tags when editing your template in Microsoft Word or LibreOffice.
---
## 1. Personal Details (Simple Text Tags)
These tags are replaced with the candidate's personal information.
| Tag | Description | Example Output |
|-----|-------------|----------------|
| `{d.fullName}` | Full name (first + last) | John Smith |
| `{d.firstName}` | First name only | John |
| `{d.lastName}` | Last name only | Smith |
| `{d.email}` | Email address | john.smith@email.com |
| `{d.phone}` | Phone number | +1-555-123-4567 |
| `{d.address}` | Physical address | Cape Town, South Africa |
| `{d.linkedin}` | LinkedIn profile URL | linkedin.com/in/johnsmith |
| `{d.github}` | GitHub profile URL | github.com/jsmith |
| `{d.website}` | Personal website | www.johnsmith.com |
| `{d.summary}` | Professional summary paragraph | Experienced software engineer... |
| `{d.generationDate}` | Date the CV was generated | July 16, 2026 |
---
## 2. Work Experience (Loop Tags)
Loop tags repeat a section of your document for each item in an array.
Carbone automatically detects loops when you use `[i]` in the tag name.
### Basic Loop Fields
| Tag | Description | Example Output |
|-----|-------------|----------------|
| `{d.experience[i].position}` | Job title | Senior Software Engineer |
| `{d.experience[i].company}` | Company name | TechCorp International |
| `{d.experience[i].location}` | Work location | Cape Town |
| `{d.experience[i].startDate}` | Start date (formatted) | Jan 2022 |
| `{d.experience[i].endDate}` | End date or "Present" | Present |
| `{d.experience[i].duration}` | Calculated duration | 4.5 years |
| `{d.experience[i].description}` | Job description | Led a team building microservices |
| `{d.experience[i].skillsUsed}` | Comma-separated skills used | C#, Azure, Docker |
### Achievements Sub-Loop
Inside the experience section, you can loop through achievements:
| Tag | Description |
|-----|-------------|
| `{d.experience[i].achievements[j]}` | Individual achievement bullet point |
### How to Set Up a Loop in Word
In your Word document, simply write the tags in the section you want repeated.
Carbone auto-detects the loop boundaries based on the document structure
(paragraphs, table rows, or list items).
Example layout in Word:
```
{d.experience[i].position} at {d.experience[i].company}
{d.experience[i].startDate} - {d.experience[i].endDate} ({d.experience[i].duration})
{d.experience[i].description}
• {d.experience[i].achievements[j]}
Skills used: {d.experience[i].skillsUsed}
```
For each job entry in the candidate's data, Carbone will duplicate this block
and fill in the values.
---
## 3. Skills (Loop Tags)
### Flat Skills List
| Tag | Description | Example Output |
|-----|-------------|----------------|
| `{d.skills[i].name}` | Skill name | C# / .NET |
| `{d.skills[i].category}` | Skill category | Programming |
| `{d.skills[i].proficiency}` | Proficiency level | Expert |
| `{d.skills[i].years}` | Years of experience (dynamic) | 8.5 |
| `{d.skills[i].startYear}` | Year started | 2018 |
### Skills Grouped by Category
Use this when you want skills organized under category headings:
| Tag | Description | Example Output |
|-----|-------------|----------------|
| `{d.skillsByCategory[i].category}` | Category name | Programming |
| `{d.skillsByCategory[i].skills[j].name}` | Skill name within category | C# / .NET |
| `{d.skillsByCategory[i].skills[j].years}` | Years of experience | 8.5 |
| `{d.skillsByCategory[i].skills[j].proficiency}` | Proficiency level | Expert |
Example layout in Word:
```
{d.skillsByCategory[i].category}:
• {d.skillsByCategory[i].skills[j].name} ({d.skillsByCategory[i].skills[j].years}y)
```
---
## 4. Education (Loop Tags)
| Tag | Description | Example Output |
|-----|-------------|----------------|
| `{d.education[i].degree}` | Degree name | BSc Computer Science |
| `{d.education[i].field}` | Field of study | Computer Science |
| `{d.education[i].institution}` | University/institution | University of Cape Town |
| `{d.education[i].startDate}` | Start date | Jan 2014 |
| `{d.education[i].endDate}` | End date | Dec 2017 |
| `{d.education[i].grade}` | Grade/result | First Class Honours |
---
## 5. Certifications (Loop Tags)
| Tag | Description | Example Output |
|-----|-------------|----------------|
| `{d.certifications[i].name}` | Certification name | Azure Developer Associate |
| `{d.certifications[i].issuer}` | Issuing organization | Microsoft |
| `{d.certifications[i].date}` | Issue date | Jun 2021 |
| `{d.certifications[i].expiryDate}` | Expiry date (if any) | Jun 2024 |
---
## 6. Conditional Tags (Show/Hide Content)
Conditionals let you show or hide content based on whether a field has a value.
### Show if NOT Empty: `:showBegin` / `:showEnd`
Only shows the content between the markers if the field has a value:
```
{d.linkedin:showBegin}LinkedIn: {d.linkedin}{d.linkedin:showEnd}
```
If the candidate has no LinkedIn, the entire "LinkedIn: ..." text is hidden.
### Hide if NOT Empty: `:hideBegin` / `:hideEnd`
Shows content only if the field IS empty (opposite of showBegin):
```
{d.website:hideBegin}No website provided{d.website:hideEnd}
```
### If Empty: `:ifEM`
Shows the replacement text if the field is empty:
```
{d.address:ifEM(Address not provided)}
```
### If Not Empty: `:ifNEM`
Shows the replacement text if the field has a value:
```
{d.email:ifNEM(Email verified)}
```
---
## 7. Formatting Tags
### Date Formatting: `:formatD`
Format dates inside your template:
```
{d.experience[i].startDate:formatD(YYYY-MM-DD)} -> 2022-01-01
{d.experience[i].startDate:formatD(MMM YYYY)} -> Jan 2022
{d.experience[i].startDate:formatD(DD/MM/YYYY)} -> 01/01/2022
```
Note: Our system already pre-formats dates as "Mon YYYY", so you typically
don't need this formatter unless you want a different format.
### Number Formatting: `:formatN`
```
{d.skills[i].years:formatN(0.0)} -> 8.5
{d.skills[i].years:formatN(0)} -> 9
```
### Case Conversion
```
{d.fullName:upperCase} -> JOHN SMITH
{d.fullName:lowerCase} -> john smith
{d.fullName:ucFirst} -> John smith
{d.fullName:ucWords} -> John Smith
```
### Text Operations
```
{d.summary:substr(0, 100)} -> First 100 characters of summary
{d.summary:slice(0, 50)} -> First 50 characters
{d.email:padl(30)} -> Padded to 30 chars on the left
{d.email:padr(30)} -> Padded to 30 chars on the right
```
---
## 8. Comparison Conditionals
These conditionals compare a field to a specific value:
### If Equal: `:ifEQ`
```
{d.skills[i].proficiency:ifEQ(Expert):showBegin}
This person is an expert!
{d.skills[i].proficiency:showEnd}
```
### If Not Equal: `:ifNE`
```
{d.experience[i].endDate:ifNE():showBegin}
(Former position)
{d.experience[i].endDate:showEnd}
```
### If Greater Than: `:ifGT`
```
{d.skills[i].years:ifGT(5):showBegin}Senior level{d.skills[i].years:showEnd}
```
### If Greater Than or Equal: `:ifGTE`
```
{d.skills[i].years:ifGTE(3):showBegin}Experienced{d.skills[i].years:showEnd}
```
### If Less Than: `:ifLT`
```
{d.skills[i].years:ifLT(2):showBegin}Junior level{d.skills[i].years:showEnd}
```
### If Contains: `:ifContain`
```
{d.skills[i].name:ifContain(Azure):showBegin}Cloud certified{d.skills[i].name:showEnd}
```
---
## 9. Array Operations
### Array Join: `:arrayJoin`
Join all items in an array with a separator:
```
{d.skills:arrayJoin(, )} -> C#, Azure, React, Docker
```
### Array Map: `:arrayMap`
Extract a specific field from all array items:
```
{d.skills:arrayMap(name):arrayJoin(, )} -> C#, Azure, React, Docker
```
### Count: `:count`
Count items in an array:
```
{d.experience:count} -> 3
{d.skills:count} -> 7
```
### Length: `:len`
String length:
```
{d.summary:len} -> 145
```
---
## 10. Complete Example Template
Here is how a complete section of your Word document might look:
```
{d.fullName}
{d.email} | {d.phone} | {d.address}
{d.linkedin:showBegin}LinkedIn: {d.linkedin} | {d.linkedin:showEnd}
{d.github:showBegin}GitHub: {d.github}{d.github:showEnd}
PROFESSIONAL SUMMARY
{d.summary}
WORK EXPERIENCE
{d.experience[i].position} at {d.experience[i].company}
{d.experience[i].startDate} - {d.experience[i].endDate} ({d.experience[i].duration})
{d.experience[i].location:showBegin}Location: {d.experience[i].location}{d.experience[i].location:showEnd}
{d.experience[i].description}
Achievements:
• {d.experience[i].achievements[j]}
Skills used: {d.experience[i].skillsUsed}
SKILLS
{d.skillsByCategory[i].category}:
• {d.skillsByCategory[i].skills[j].name} ({d.skillsByCategory[i].skills[j].years}y)
{d.skillsByCategory[i].skills[j].proficiency:showBegin} - {d.skillsByCategory[i].skills[j].proficiency}{d.skillsByCategory[i].skills[j].proficiency:showEnd}
EDUCATION
{d.education[i].degree} in {d.education[i].field}
{d.education[i].institution}
{d.education[i].startDate} - {d.education[i].endDate}
{d.education[i].grade:showBegin}Grade: {d.education[i].grade}{d.education[i].grade:showEnd}
CERTIFICATIONS
• {d.certifications[i].name} - {d.certifications[i].issuer} ({d.certifications[i].date})
Generated on {d.generationDate}
```
---
## Quick Reference Card
| Syntax | Meaning |
|--------|---------|
| `{d.field}` | Simple text replacement |
| `{d.array[i].field}` | Loop - repeats for each array item |
| `{d.array[i].subarray[j].field}` | Nested loop |
| `:showBegin` / `:showEnd` | Show content if field is not empty |
| `:hideBegin` / `:hideEnd` | Show content if field is empty |
| `:ifEM(text)` | Show text if field is empty |
| `:ifNEM(text)` | Show text if field is not empty |
| `:ifEQ(value)` | Show if field equals value |
| `:ifNE(value)` | Show if field does not equal value |
| `:ifGT(value)` | Show if field is greater than value |
| `:ifGTE(value)` | Show if field is greater than or equal |
| `:ifLT(value)` | Show if field is less than value |
| `:ifLTE(value)` | Show if field is less than or equal |
| `:ifContain(text)` | Show if field contains text |
| `:formatD(YYYY-MM-DD)` | Format date |
| `:formatN(0.0)` | Format number |
| `:upperCase` / `:lowerCase` | Case conversion |
| `:ucFirst` / `:ucWords` | Capitalize first letter / each word |
| `:arrayJoin(, )` | Join array with separator |
| `:count` | Count array items |
| `:substr(0, 100)` | Substring |
| `:padl(30)` / `:padr(30)` | Pad string to length |

View File

@@ -150,3 +150,37 @@ async function deleteCarboneTemplate(templateId) {
toast('Delete failed: ' + e.message, 'error');
}
}
// ============================================================
// TAGS HELP - View in app
// ============================================================
async function viewTagsHelp() {
try {
const resp = await fetch(CARBONE_API + '/help');
const data = await resp.json();
let html = '<div style="max-height:600px;overflow-y:auto">';
for (const section of data.sections) {
html += `<h3 style="margin-top:20px;color:var(--accent)">${section.title}</h3>`;
if (section.note) html += `<p class="text-muted text-sm" style="margin-bottom:8px">${section.note}</p>`;
html += '<table><thead><tr><th>Tag</th><th>Description</th></tr></thead><tbody>';
for (const t of section.tags) {
html += `<tr><td style="font-family:monospace;color:var(--accent);white-space:nowrap">${escapeHtml(t.tag)}</td><td>${escapeHtml(t.desc)}</td></tr>`;
}
html += '</tbody></table>';
}
html += '</div>';
html += '<div class="mt-16"><button class="btn btn-outline" onclick="downloadHelpDocx()">Download as .docx</button></div>';
showModal(html, 'Carbone Tags Reference');
} catch (e) {
toast('Failed to load help: ' + e.message, 'error');
}
}
// ============================================================
// TAGS HELP - Download .docx
// ============================================================
function downloadHelpDocx() {
window.open(CARBONE_API + '/help-docx', '_blank');
}

View File

@@ -73,6 +73,8 @@
4. Generate CVs by merging candidate data with your template -> PDF
</div>
<button class="btn mt-16" onclick="downloadMasterTemplate()">Download Master Template (.docx)</button>
<button class="btn btn-outline mt-16" style="margin-left:8px" onclick="viewTagsHelp()">View Tags Help</button>
<button class="btn btn-outline mt-16" style="margin-left:8px" onclick="downloadHelpDocx()">Download Help (.docx)</button>
</div>
<div class="card mb-16">
<div class="card-header"><span class="card-title">Upload Custom Template</span></div>