diff --git a/renderer/carbone-service.js b/renderer/carbone-service.js index 7fabf9b..847e1dc 100644 --- a/renderer/carbone-service.js +++ b/renderer/carbone-service.js @@ -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) { diff --git a/renderer/carbone-templates/HELP_TAGS_REFERENCE.docx b/renderer/carbone-templates/HELP_TAGS_REFERENCE.docx new file mode 100644 index 0000000..33d755a Binary files /dev/null and b/renderer/carbone-templates/HELP_TAGS_REFERENCE.docx differ diff --git a/renderer/carbone-templates/HELP_TAGS_REFERENCE.md b/renderer/carbone-templates/HELP_TAGS_REFERENCE.md new file mode 100644 index 0000000..a1a9ca6 --- /dev/null +++ b/renderer/carbone-templates/HELP_TAGS_REFERENCE.md @@ -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 | \ No newline at end of file diff --git a/static/carbone.js b/static/carbone.js index ef1324e..644dfd6 100644 --- a/static/carbone.js +++ b/static/carbone.js @@ -149,4 +149,38 @@ async function deleteCarboneTemplate(templateId) { } catch (e) { 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 = '
${section.note}
`; + html += '| Tag | Description |
|---|---|
| ${escapeHtml(t.tag)} | ${escapeHtml(t.desc)} |