diff --git a/package.json b/package.json
index 7755a2e..e033cde 100644
--- a/package.json
+++ b/package.json
@@ -22,6 +22,9 @@
"vite-plugin-singlefile": "^0.13.2"
},
"dependencies": {
+ "@citation-js/core": "^0.7.1",
+ "@citation-js/plugin-bibtex": "^0.7.1",
+ "@citation-js/plugin-csl": "^0.7.14",
"@iconify/json": "^2.2.15",
"@json-editor/json-editor": "^2.9.0-beta.1",
"ajv": "^8.12.0",
diff --git a/sample/sample.cv.prof.json b/sample/sample.cv.prof.json
new file mode 100644
index 0000000..e604c7e
--- /dev/null
+++ b/sample/sample.cv.prof.json
@@ -0,0 +1,758 @@
+{
+ "$schema": "https://raw.githubusercontent.com/cuihantao/jsoncv/refs/heads/master/schema/jsoncv.schema.prof.json",
+ "basics": {
+ "name": "Hantao Cui",
+ "label": "Associate Professor",
+ "image": "",
+ "email": "hcui9@ncsu.edu",
+ "phone": "(919) 513-2024",
+ "url": "https://cui.eecps.com",
+ "summary": "Research focuses on computational and analytical techniques for power systems: modeling, simulation, stability analysis, control, and software engineering",
+ "location": {
+ "address": "21 Keystone Science Center, 1791 Varsity Dr.",
+ "postalCode": "27606",
+ "city": "Raleigh",
+ "region": "NC",
+ "countryCode": "US"
+ },
+ "profiles": [
+ {
+ "network": "Google Scholar",
+ "username": "Hantao Cui",
+ "url": "https://scholar.google.com/citations?user=yvsfeQEAAAAJ"
+ },
+ {
+ "network": "LinkedIn",
+ "username": "Hantao Cui",
+ "url": "https://www.linkedin.com/in/hantao-cui/"
+ }
+ ]
+ },
+ "work": [
+ {
+ "name": "North Carolina State University",
+ "location": "Raleigh, NC",
+ "description": "Department of Electrical and Computer Engineering",
+ "position": "Associate Professor",
+ "url": "https://www.ncsu.edu",
+ "startDate": "2024-08-16",
+ "summary": "Research in power systems modeling and simulation, high-performance computing, and machine learning applications"
+ },
+ {
+ "name": "Oklahoma State University",
+ "location": "Stillwater, OK",
+ "description": "School of Electrical and Computer Engineering",
+ "position": "Assistant Professor",
+ "url": "https://www.okstate.edu",
+ "startDate": "2021-08-01",
+ "endDate": "2024-08-15",
+ "summary": "Research and teaching in power systems and scientific computing"
+ },
+ {
+ "name": "University of Tennessee, Knoxville",
+ "location": "Knoxville, TN",
+ "description": "Department of Electrical Engineering and Computer Science",
+ "position": "Research Assistant Professor",
+ "url": "https://www.utk.edu",
+ "startDate": "2019-04-01",
+ "endDate": "2021-07-31",
+ "summary": "Research in power system simulation and open-source software development"
+ },
+ {
+ "name": "University of Tennessee, Knoxville",
+ "location": "Knoxville, TN",
+ "description": "Center for Ultra-Wide-Area Resilient Electric Transmission Networks (CURENT)",
+ "position": "Research Associate",
+ "url": "https://www.utk.edu",
+ "startDate": "2017-01-01",
+ "endDate": "2019-04-30",
+ "summary": "Research in large-scale power system simulation and cyber-physical systems"
+ }
+ ],
+ "volunteer": [
+ {
+ "organization": "2027 North American Power Symposium",
+ "location": "Raleigh, NC",
+ "position": "Co-Chair",
+ "url": "",
+ "startDate": "2024",
+ "endDate": "2027",
+ "summary": "Co-chairing the organization of the 2027 North American Power Symposium",
+ "highlights": []
+ },
+ {
+ "organization": "NSF CSSI-CyberTraining PI Meeting",
+ "location": "Charlotte, NC",
+ "position": "Organizing Committee Member",
+ "url": "",
+ "startDate": "2024-04-01",
+ "endDate": "2024-08-31",
+ "summary": "Served on the organizing committee for the NSF CSSI-CyberTraining PI Meeting",
+ "highlights": []
+ },
+ {
+ "organization": "IEEE Power and Energy Society",
+ "position": "Vice Chair and TCPC",
+ "url": "",
+ "startDate": "2023-01-01",
+ "summary": "Vice Chair and TCPC of Computer and Analytical Methods Subcommittee (CAMS)",
+ "highlights": []
+ },
+ {
+ "organization": "IEEE Power and Energy Society",
+ "position": "Secretary",
+ "url": "",
+ "startDate": "2018-08-01",
+ "endDate": "2023-01-01",
+ "summary": "Secretary of Ultra-Wide-Area HVDC Overlay Studies Task Force",
+ "highlights": []
+ }
+ ],
+ "education": [
+ {
+ "institution": "University of Tennessee, Knoxville",
+ "location": "Tennessee, USA",
+ "url": "https://www.utk.edu",
+ "area": "Electrical Engineering and Computer Science",
+ "studyType": "Ph.D.",
+ "startDate": "2013-08-20",
+ "endDate": "2018-12-18",
+ "score": "",
+ "dissertation": "Large-Scale Simulation of Modern Electric Power Systems",
+ "courses": []
+ },
+ {
+ "institution": "Southeast University",
+ "location": "Nanjing, China",
+ "url": "https://www.seu.edu.cn",
+ "area": "Electrical Engineering",
+ "studyType": "M.S.",
+ "startDate": "2011-09-01",
+ "endDate": "2013-06-30",
+ "score": "",
+ "courses": []
+ },
+ {
+ "institution": "Southeast University",
+ "location": "Nanjing, China",
+ "url": "https://www.seu.edu.cn",
+ "area": "Electrical Engineering | Chien-Shiung Wu Honor College",
+ "studyType": "B.S.",
+ "startDate": "2007-09-01",
+ "endDate": "2011-06-30",
+ "score": "",
+ "courses": []
+ }
+ ],
+ "awards": [
+ {
+ "title": "IEEE PES PSOPE Committee Prize Paper Award",
+ "date": "2024-02-01",
+ "awarder": "IEEE Power and Energy Society"
+ },
+ {
+ "title": "National Science Foundation CAREER Award",
+ "date": "2024-01-01",
+ "awarder": "National Science Foundation"
+ },
+ {
+ "title": "Best Conference Paper, 3rd Place",
+ "date": "2023-10-01",
+ "awarder": "North American Power Symposium (NAPS)"
+ },
+ {
+ "title": "Best Conference Paper",
+ "date": "2022-07-01",
+ "awarder": "IEEE PES General Meeting"
+ },
+ {
+ "title": "Outstanding Reviewer for 2020",
+ "date": "2021-01-30",
+ "awarder": "IEEE Transactions on Power Systems"
+ },
+ {
+ "title": "R&D 100 Award for CURENT Large-Scale Testbed",
+ "date": "2020-09-30",
+ "awarder": "R&D Magazine"
+ },
+ {
+ "title": "Outstanding Reviewer for 2019",
+ "date": "2020-03-01",
+ "awarder": "IEEE Transactions on Power Systems"
+ },
+ {
+ "title": "Highly Cited Paper Award",
+ "date": "2019-07-01",
+ "awarder": "Applied Energy"
+ },
+ {
+ "title": "Top Peer Reviewer (Top 1% in Engineering)",
+ "date": "2018-09-30",
+ "awarder": "Publons.com"
+ },
+ {
+ "title": "ESI Highly Cited Papers",
+ "date": "2018-03-01",
+ "awarder": "Essential Science Indicators"
+ },
+ {
+ "title": "Chancellor's Citation on Extraordinary Professional Promise",
+ "date": "2017-04-18",
+ "awarder": "University of Tennessee, Knoxville"
+ },
+ {
+ "title": "Best Conference Paper",
+ "date": "2016-07-01",
+ "awarder": "IEEE PES General Meeting"
+ }
+ ],
+ "certificates": [
+ {
+ "name": "IEEE Senior Member",
+ "date": "2020-12-20",
+ "issuer": "IEEE",
+ "url": ""
+ }
+ ],
+ "publications": [
+ {
+ "name": "Evaluating the Equivalent Inertia of Grid-Following and Grid-Forming Inverter-Based Resources",
+ "publisher": "IEEE Transactions on Energy Conversion",
+ "releaseDate": "2024",
+ "url": "https://doi.org/10.1109/TEC.2024.3520383",
+ "summary": "Novel method for estimating equivalent inertia in modern power systems with inverter-based resources"
+ },
+ {
+ "name": "Bus Admittance Matrix Revisited: Performance Challenges on Modern Computers",
+ "publisher": "IEEE Open Access Journal of Power and Energy",
+ "releaseDate": "2024",
+ "url": "https://doi.org/10.1109/OAJPE.2024.3366117",
+ "summary": "Analysis of computational performance challenges in power system matrix operations on modern computing architectures"
+ },
+ {
+ "name": "Virtual Inertia Scheduling (VIS) for Microgrids with Static and Dynamic Security Constraints",
+ "publisher": "IEEE Transactions on Sustainable Energy",
+ "releaseDate": "2024",
+ "url": "https://doi.org/10.1109/TSTE.2024.3481239",
+ "summary": "Novel approach to scheduling virtual inertia in microgrids considering both static and dynamic security constraints"
+ },
+ {
+ "name": "Hybrid Symbolic-Numeric Framework for Power System Modeling and Analysis",
+ "publisher": "IEEE Transactions on Power Systems",
+ "releaseDate": "2020",
+ "url": "https://doi.org/10.1109/TPWRS.2020.3017019",
+ "summary": "Innovative framework combining symbolic and numerical methods for power system modeling and simulation"
+ },
+ {
+ "name": "Power Electronics-Interfaced Cyber-Physical Power Systems: A Review on Modeling, Simulation, and Cybersecurity",
+ "publisher": "WIREs Energy and Environment",
+ "releaseDate": "2022",
+ "url": "",
+ "summary": "Comprehensive review of modeling, simulation, and cybersecurity aspects in modern power systems with power electronic interfaces"
+ }
+ ],
+ "skills": [
+ {
+ "name": "Power Systems",
+ "level": "Expert",
+ "summary": "Expert in power system modeling, simulation, stability analysis, and control systems",
+ "keywords": [
+ "Modeling and Simulation",
+ "Stability Analysis",
+ "Control Systems",
+ "High Performance Computing",
+ "Software Engineering",
+ "Machine Learning Applications"
+ ]
+ },
+ {
+ "name": "Software Development",
+ "level": "Expert",
+ "summary": "Extensive experience in scientific computing and high-performance software development",
+ "keywords": [
+ "Python",
+ "Julia",
+ "High Performance Computing",
+ "Open Source Development",
+ "Symbolic Computing"
+ ]
+ }
+ ],
+ "languages": [
+ {
+ "language": "English",
+ "fluency": "Professional",
+ "summary": "Professional working proficiency in academic and technical contexts"
+ },
+ {
+ "language": "Chinese",
+ "fluency": "Native",
+ "summary": "Native speaker"
+ }
+ ],
+ "interests": [
+ {
+ "name": "Power Systems Research",
+ "keywords": [
+ "Grid Modeling",
+ "Stability Analysis",
+ "Control Systems",
+ "Renewable Integration",
+ "High Performance Computing"
+ ]
+ },
+ {
+ "name": "Software Development",
+ "keywords": [
+ "Open Source",
+ "Power System Simulation",
+ "High Performance Computing",
+ "Machine Learning"
+ ]
+ }
+ ],
+ "projects": [
+ {
+ "name": "ANDES",
+ "description": "Python Software for Power System Modeling and Simulation",
+ "summary": "Python Software for Power System Modeling and Simulation. A unique hybrid symbolic-numeric framework that enables descriptive DAE modeling, numerical code generation, and just-in-time compilation for simulation.",
+ "highlights": [
+ "Over 4,000 code commits",
+ "170+ GitHub stars",
+ "80+ forks",
+ "Used in multiple NSF and DOE projects"
+ ],
+ "keywords": ["Python", "Power Systems", "Simulation"],
+ "startDate": "2015",
+ "endDate": "Present",
+ "url": "https://github.com/curent/andes",
+ "roles": ["Lead Developer"],
+ "entity": "CURENT",
+ "type": "Software"
+ },
+ {
+ "name": "NSF CAREER Award",
+ "description": "Multi-Timescale Dynamics Modeling, Simulation, and Analysis of Converter-Dominated Power Systems",
+ "summary": "Multi-Timescale Dynamics Modeling, Simulation, and Analysis of Converter-Dominated Power Systems",
+ "highlights": [
+ "Principal Investigator",
+ "Research on power system dynamics"
+ ],
+ "keywords": ["Power Systems", "Dynamics", "Simulation"],
+ "startDate": "2024-09",
+ "endDate": "2029-08",
+ "url": "",
+ "roles": ["Principal Investigator"],
+ "entity": "NSF",
+ "type": "Research",
+ "projectType": "research",
+ "fundingDetails": {
+ "role": "PI",
+ "fundingAgency": "NSF",
+ "amount": "$500,000",
+ "status": "active"
+ }
+ },
+ {
+ "name": "POSE",
+ "description": "Toward an Open-Source Ecosystem for Power Systems Research, Education, and Industry Applications",
+ "summary": "Toward an Open-Source Ecosystem for Power Systems Research, Education, and Industry Applications",
+ "highlights": [
+ "NSF TIP funded project",
+ "Principal Investigator",
+ "Open-source power system software"
+ ],
+ "keywords": ["Open Source", "Power Systems", "Education"],
+ "startDate": "2024-09",
+ "endDate": "2025-08",
+ "url": "",
+ "roles": ["Principal Investigator"],
+ "entity": "NSF",
+ "type": "Research",
+ "projectType": "research",
+ "fundingDetails": {
+ "role": "PI",
+ "fundingAgency": "NSF",
+ "amount": "$300,000",
+ "status": "active"
+ }
+ },
+ {
+ "name": "HVDC-Learn",
+ "description": "Modular Education & Workforce Training in High Voltage Direct Current Electric Transmission",
+ "summary": "Modular Education & Workforce Training in High Voltage Direct Current Electric Transmission",
+ "highlights": [
+ "Co-Principal Investigator",
+ "Multi-university collaboration",
+ "Education and workforce development"
+ ],
+ "keywords": ["Education", "HVDC", "Power Systems"],
+ "startDate": "2024-01",
+ "endDate": "2026-12",
+ "url": "",
+ "roles": ["Co-Principal Investigator"],
+ "entity": "DOE",
+ "type": "Education",
+ "projectType": "education",
+ "fundingDetails": {
+ "role": "co-PI",
+ "fundingAgency": "DOE",
+ "amount": "$48,875",
+ "status": "active"
+ }
+ },
+ {
+ "name": "PowerCyber",
+ "description": "CyberTraining: Pilot: PowerCyber: Computational Training for Power Engineering Researchers",
+ "summary": "CyberTraining: Pilot: PowerCyber: Computational Training for Power Engineering Researchers",
+ "highlights": [
+ "Lead Principal Investigator",
+ "Computational training development"
+ ],
+ "keywords": ["Education", "Power Systems", "Computation"],
+ "startDate": "2024-01",
+ "endDate": "2025-12",
+ "url": "",
+ "roles": ["Lead Principal Investigator"],
+ "entity": "NSF",
+ "type": "Education",
+ "projectType": "education",
+ "fundingDetails": {
+ "role": "PI",
+ "fundingAgency": "NSF",
+ "amount": "$180,000",
+ "status": "active"
+ }
+ },
+ {
+ "name": "High-Performance Power System Simulation",
+ "description": "High-Performance Transient Stability Simulation of Power Systems on Modern Parallel Computing Hardware",
+ "summary": "High-Performance Transient Stability Simulation of Power Systems on Modern Parallel Computing Hardware",
+ "highlights": [
+ "Principal Investigator",
+ "Advanced computing research"
+ ],
+ "keywords": ["HPC", "Power Systems", "Simulation"],
+ "startDate": "2022-09",
+ "endDate": "2025-08",
+ "url": "",
+ "roles": ["Principal Investigator"],
+ "entity": "NSF",
+ "type": "Research",
+ "projectType": "research",
+ "fundingDetails": {
+ "role": "PI",
+ "fundingAgency": "NSF",
+ "amount": "$320,000",
+ "status": "active"
+ }
+ }
+ ],
+ "mentoring": {
+ "current_students": [
+ {
+ "name": "Ahmad Ali",
+ "level": "Ph.D.",
+ "startDate": "2022-01",
+ "url": "",
+ "dissertation": "High-Performance Computing for Power System Dynamic Simulation"
+ }
+ ],
+ "committee_service": [
+ {
+ "name": "Zaid Ibn Mahmood",
+ "institution": "Oklahoma State University",
+ "status": "Current"
+ }
+ ],
+ "past_students": [
+ {
+ "name": "Haya Monawwar",
+ "level": "Ph.D. Student",
+ "period": "Fall 23, Spring 24",
+ "institution": "Oklahoma State University",
+ "url": ""
+ },
+ {
+ "name": "Karter Caves",
+ "level": "Undergraduate",
+ "period": "Fall 23, Spring 24",
+ "program": "Freshman Research Scholar",
+ "institution": "Oklahoma State University",
+ "url": ""
+ },
+ {
+ "name": "Evelyn Wilson",
+ "level": "Undergraduate",
+ "period": "Spring 22, Fall 23",
+ "institution": "Oklahoma State University"
+ },
+ {
+ "name": "Paul Magna",
+ "level": "Undergraduate",
+ "period": "Fall 21",
+ "institution": "Oklahoma State University"
+ }
+ ]
+ },
+ "teaching": [
+ {
+ "courseCode": "ECEN 5113",
+ "courseName": "Power System Analysis by Computer Methods",
+ "description": "Advanced course covering numerical methods for power system analysis, including power flow, optimal power flow, and dynamic simulation. Emphasis on high-performance computing and modern software development practices.",
+ "institution": "Oklahoma State University",
+ "terms": ["S'23", "F'21"]
+ },
+ {
+ "courseCode": "ECEN 4153",
+ "courseName": "Power System Analysis and Design",
+ "description": "Fundamentals of power system analysis, including per-unit systems, power flow studies, economic operation, and symmetrical components. Introduction to power system stability and protection.",
+ "institution": "Oklahoma State University",
+ "terms": ["F'22", "S'24"]
+ },
+ {
+ "courseCode": "ECEN 4283",
+ "courseName": "Computer Networks",
+ "institution": "Oklahoma State University",
+ "terms": ["S'22", "F'23"]
+ },
+ {
+ "courseCode": "ECE 421",
+ "courseName": "Electric Energy Systems (co-instructor)",
+ "institution": "University of Tennessee, Knoxville",
+ "terms": ["F'19"]
+ },
+ {
+ "courseCode": "ECE 496/691",
+ "courseName": "Power and Energy Systems Seminar",
+ "institution": "University of Tennessee, Knoxville",
+ "terms": ["F'19", "S'21"]
+ }
+ ],
+ "researchAreas": [
+ {
+ "name": "Advanced Grid Modeling, Simulation, and Stability Analysis",
+ "contributions": [
+ "Proposed a symbolic-numeric framework for rapid prototyping of dynamic models",
+ "Proposed a communication-in-the-loop testbed architecture for wide-area control",
+ "Reviewed power electronics control and cybersecurity challenges",
+ "Studied disturbance propagation in systems with grid-following and grid-forming converters"
+ ]
+ },
+ {
+ "name": "High-Performance Computing",
+ "contributions": [
+ "Effective parallelism for accelerating equation and Jacobian evaluation in power flow",
+ "Revisit the computational performance of the Ybus method on modern computers with data parallelism",
+ "Understanding the performance of preconditioned iterative methods for fast-decoupled power flow on GPUs"
+ ]
+ },
+ {
+ "name": "Emerging Learning Techniques for Control",
+ "contributions": [
+ "Development of open-source Andes-Gym environment to support reinforcement learning-based control in power systems",
+ "Using machine learning to integrate dynamic response in a unit-commitment problem",
+ "Transmission and distribution co-simulation and control"
+ ]
+ }
+ ],
+ "services": {
+ "editorialBoards": [
+ {
+ "role": "Associate Editor",
+ "journal": "IET Energy Conversion and Economics",
+ "period": "2012-Present"
+ },
+ {
+ "role": "Associate Editor",
+ "journal": "Protection and Control of Modern Power Systems",
+ "period": "2012-Present"
+ },
+ {
+ "role": "Associate Editor",
+ "journal": "Journal of Modern Power Systems and Clean Energy",
+ "period": "2019-Present"
+ },
+ {
+ "role": "Guest Editor",
+ "journal": "Frontiers in Energy Research",
+ "period": "2022",
+ "details": "Special Issue: Advances in Distributed Energy Resources Aggregation for the Low Carbon Future"
+ },
+ {
+ "role": "Guest Editor",
+ "journal": "IET Generation, Transmission and Distribution",
+ "period": "2021"
+ }
+ ],
+ "societyServices": [
+ {
+ "role": "Vice Chair and TCPC",
+ "organization": "Computer and Analytical Methods Subcommittee (CAMS), IEEE Power and Energy Society",
+ "period": "2023-Present"
+ },
+ {
+ "role": "Secretary",
+ "organization": "Ultra-Wide-Area HVDC Overlay Studies Task Force, IEEE Power and Energy Society",
+ "period": "2018-Present"
+ },
+ {
+ "role": "Member",
+ "organization": "Power System Stability Control Subcommittee",
+ "period": "2022-Present"
+ }
+ ],
+ "reviewServices": [
+ {
+ "type": "Proposal Review",
+ "organization": "National Science Foundation",
+ "year": "2024",
+ "details": "Reviewer for three panels"
+ },
+ {
+ "type": "Proposal Review",
+ "organization": "Department of Energy",
+ "year": "2023",
+ "details": "Panel Reviewer for two panels"
+ }
+ ],
+ "departmental": [
+ {
+ "role": "Member",
+ "committee": "Colloquium Committee",
+ "period": "2024-Present"
+ }
+ ]
+ },
+ "presentations": {
+ "invitedTalks": [
+ {
+ "venue": "IEEE PES Young Professional Phoenix Chapter",
+ "type": "Webinar",
+ "date": "2024-05",
+ "title": "The Pursuit for a Generalized Simulation Framework for Power System Transients and Stability"
+ },
+ {
+ "venue": "Stony Brook University",
+ "type": "Webinar",
+ "date": "2024-05",
+ "title": "Hybrid-Symbolic Numeric Framework for Power System Dynamics: ANDES Package and Future Directions"
+ },
+ {
+ "venue": "NC State University",
+ "type": "Seminar",
+ "date": "2024-03",
+ "title": "The New Dynamics: Computational Solutions to Modeling and Simulation of Sustainable Power Systems"
+ },
+ {
+ "venue": "Clarkson University",
+ "type": "Webinar",
+ "date": "2024-03",
+ "title": "Hybrid-Symbolic Numeric Framework forPower System Dynamics: ANDES Package and Future Directions"
+ },
+ {
+ "venue": "SUNY Buffalo",
+ "type": "Seminar",
+ "date": "2024-03",
+ "title": "The New Dynamics: Computational Solutions to Modeling and Simulation of Sustainable Power Systems"
+ },
+ {
+ "venue": "Southern Methodist University",
+ "type": "Seminar",
+ "date": "2024-03",
+ "title": "The New Dynamics: Computational Solutions to Modeling and Simulation of Sustainable Power Systems"
+ },
+ {
+ "venue": "Rowan University",
+ "type": "Seminar",
+ "date": "2024-02",
+ "title": "The New Dynamics: Computational Solutions to Modeling and Simulation of Sustainable Power Systems"
+ },
+ {
+ "venue": "University of Georgia",
+ "type": "Seminar",
+ "date": "2023-02",
+ "title": "Advancing Cyber-Physical Power Systems Research Using Modern Computer Technologies"
+ },
+ {
+ "venue": "University of Houston",
+ "type": "Webinar",
+ "date": "2022-11",
+ "title": "Symbolics-Assisted Modeling for High-Performance Power System Simulation"
+ },
+ {
+ "venue": "Washington State University",
+ "type": "Webinar",
+ "date": "2022-10",
+ "title": "Composable, Expressive and Portable Modeling for High-Performance Power System Simulation"
+ },
+ {
+ "venue": "CURENT Industry Members",
+ "type": "Presentation",
+ "date": "2022-06",
+ "title": "Disturbance Propagation in Power Grids With High Converter Penetration"
+ }
+ ],
+ "conferences": [
+ {
+ "title": "Andes_gym: A Versatile Environment for Deep Reinforcement Learning in Power Systems",
+ "venue": "2022 IEEE PESGM",
+ "location": "Denver",
+ "date": "2022-06",
+ "type": "Best Paper Presentation",
+ "description": "Presented a novel open-source environment for applying reinforcement learning to power system control problems, featuring integration with ANDES for high-fidelity power system simulation.",
+ "url": ""
+ },
+ {
+ "title": "Application of Electric Grid Simulators for Education",
+ "venue": "2021 IEEE PESGM",
+ "location": "Online",
+ "date": "2021-07",
+ "type": "Panel Session Presentation",
+ "description": "Discussed innovative approaches to using power system simulators in engineering education, focusing on hands-on learning and real-world applications.",
+ "url": ""
+ },
+ {
+ "title": "HVDC Overlays in Testbeds",
+ "venue": "2019 IEEE PESGM",
+ "location": "Atlanta",
+ "date": "2019-08",
+ "type": "Panel Session Presentation",
+ "url": ""
+ }
+ ],
+ "outreach": [
+ {
+ "activity": "Faculty mentor for OSU CEAT Summer Bridge",
+ "date": "2023-07",
+ "description": "Mentored incoming engineering students during the summer bridge program, introducing them to power systems engineering and research opportunities in renewable energy integration."
+ },
+ {
+ "activity": "Organizer, Student visit to Central Rural Electric Coop, Stillwater",
+ "date": "2022-11",
+ "description": "Organized and led a field trip for undergraduate students to learn about electric cooperative operations, smart grid technologies, and career opportunities in power distribution."
+ },
+ {
+ "activity": "Co-Chaired Panel Session at 2022 IEEE PESGM: Combining physics-based and data-driven modeling and simulation for power systems",
+ "date": "2022-08"
+ },
+ {
+ "activity": "Faculty mentor for OSU CEAT Summer Bridge",
+ "date": "2022-07"
+ },
+ {
+ "activity": "Faculty mentor for OSU Discovery Day",
+ "date": "2022-04"
+ }
+ ]
+ },
+ "meta": {
+ "canonical": "https://raw.githubusercontent.com/cuihantao/jsoncv/refs/heads/master/schema/jsoncv.schema.prof.json",
+ "lastModified": "2025-01-02T00:00:00Z",
+ "colorPrimary": "#CC0000",
+ "citationStyles": {
+ "owner": "bold",
+ "mentee": "plus"
+ }
+ }
+}
\ No newline at end of file
diff --git a/schema/jsoncv.schema.json b/schema/jsoncv.schema.json
index 5fe30dd..29fc814 100644
--- a/schema/jsoncv.schema.json
+++ b/schema/jsoncv.schema.json
@@ -284,30 +284,60 @@
},
"publications": {
"type": "array",
- "description": "Specify your publications through your career",
"items": {
"type": "object",
- "additionalProperties": true,
"properties": {
"name": {
"type": "string",
- "description": "e.g. The World Wide Web"
+ "description": "Title of the publication"
+ },
+ "authors": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "description": "List of authors with highlighting markers"
},
- "publisher": {
+ "venue": {
"type": "string",
- "description": "e.g. IEEE, Computer Magazine"
+ "description": "Journal or conference name"
},
"releaseDate": {
- "$ref": "#/definitions/iso8601"
+ "type": "string",
+ "description": "Publication year"
},
"url": {
"type": "string",
- "description": "e.g. http://www.computer.org.example.com/csdl/mags/co/1996/10/rx069-abs.html",
- "format": "uri"
+ "format": "uri",
+ "description": "DOI or URL to the publication"
},
"summary": {
"type": "string",
- "description": "Short summary of publication. e.g. Discussion of the World Wide Web, HTTP, HTML."
+ "description": "Abstract or summary"
+ },
+ "citation": {
+ "type": "string",
+ "description": "Pre-formatted citation"
+ },
+ "formattedHTML": {
+ "type": "string",
+ "description": "HTML-formatted citation"
+ },
+ "type": {
+ "type": "string",
+ "enum": ["journal", "conference", "other"],
+ "description": "Type of publication"
+ },
+ "highlightInfo": {
+ "type": "object",
+ "properties": {
+ "hasOwner": {
+ "type": "boolean"
+ },
+ "hasMentee": {
+ "type": "boolean"
+ }
+ }
}
}
}
@@ -516,11 +546,27 @@
},
"version": {
"type": "string",
- "description": "A version field which follows semver - e.g. v1.0.0"
+ "description": "Version of the CV"
},
"lastModified": {
"type": "string",
- "description": "Using ISO 8601 with YYYY-MM-DDThh:mm:ss"
+ "description": "Last modified time of the CV"
+ },
+ "colorPrimary": {
+ "type": "string",
+ "description": "Primary color for the CV theme"
+ },
+ "pageSize": {
+ "type": "string",
+ "description": "Page size for the CV (A4 or letter)",
+ "enum": ["A4", "letter"],
+ "default": "A4"
+ },
+ "theme": {
+ "type": "string",
+ "description": "Theme for the CV",
+ "enum": ["reorx", "cuiv"],
+ "default": "reorx"
}
}
}
diff --git a/schema/jsoncv.schema.prof.json b/schema/jsoncv.schema.prof.json
new file mode 100644
index 0000000..8028fb9
--- /dev/null
+++ b/schema/jsoncv.schema.prof.json
@@ -0,0 +1,1046 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema",
+ "additionalProperties": false,
+ "definitions": {
+ "iso8601": {
+ "type": "string",
+ "description": "e.g. 2014-06-29",
+ "format": "date",
+ "pattern": "^([1-2][0-9]{3}-[0-1][0-9]-[0-3][0-9]|[1-2][0-9]{3}-[0-1][0-9]|[1-2][0-9]{3})$"
+ }
+ },
+ "properties": {
+ "$schema": {
+ "type": "string",
+ "description": "link to the version of the schema that can validate the CV",
+ "format": "uri"
+ },
+ "basics": {
+ "type": "object",
+ "additionalProperties": true,
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "label": {
+ "type": "string",
+ "description": "e.g. Web Developer"
+ },
+ "image": {
+ "type": "string",
+ "description": "URL (as per RFC 3986) to a image in JPEG or PNG format"
+ },
+ "email": {
+ "type": "string",
+ "description": "e.g. thomas@gmail.com",
+ "format": "email"
+ },
+ "phone": {
+ "type": "string",
+ "description": "Phone numbers are stored as strings so use any format you like, e.g. 712-117-2923"
+ },
+ "url": {
+ "type": "string",
+ "description": "URL (as per RFC 3986) to your website, e.g. personal homepage",
+ "format": "uri"
+ },
+ "summary": {
+ "type": "string",
+ "description": "Write a short 2-3 sentence biography about yourself"
+ },
+ "location": {
+ "type": "object",
+ "additionalProperties": true,
+ "properties": {
+ "address": {
+ "type": "string",
+ "description": "To add multiple address lines, use \n. For example, 1234 Glücklichkeit Straße\nHinterhaus 5. Etage li."
+ },
+ "postalCode": {
+ "type": "string"
+ },
+ "city": {
+ "type": "string"
+ },
+ "countryCode": {
+ "type": "string",
+ "description": "code as per ISO-3166-1 ALPHA-2, e.g. US, AU, IN"
+ },
+ "region": {
+ "type": "string",
+ "description": "The general region where you live. Can be a US state, or a province, for instance."
+ }
+ }
+ },
+ "profiles": {
+ "type": "array",
+ "description": "Specify any number of social networks that you participate in",
+ "items": {
+ "type": "object",
+ "properties": {
+ "network": {
+ "type": "string",
+ "description": "The name of the social network (e.g. LinkedIn, Google Scholar, Scopus)",
+ "enum": ["LinkedIn", "Google Scholar", "Scopus"]
+ },
+ "username": {
+ "type": "string",
+ "description": "e.g. your-profile-name"
+ },
+ "url": {
+ "type": "string",
+ "description": "Your profile URL on this network",
+ "format": "uri"
+ }
+ },
+ "required": ["network", "url"]
+ }
+ }
+ }
+ },
+ "work": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "additionalProperties": true,
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "e.g. Facebook"
+ },
+ "location": {
+ "type": "string",
+ "description": "e.g. Menlo Park, CA"
+ },
+ "description": {
+ "type": "string",
+ "description": "e.g. Social Media Company"
+ },
+ "position": {
+ "type": "string",
+ "description": "e.g. Software Engineer"
+ },
+ "url": {
+ "type": "string",
+ "description": "e.g. http://facebook.example.com",
+ "format": "uri"
+ },
+ "startDate": {
+ "$ref": "#/definitions/iso8601"
+ },
+ "endDate": {
+ "$ref": "#/definitions/iso8601"
+ },
+ "summary": {
+ "type": "string",
+ "description": "Give an overview of your responsibilities at the company"
+ },
+ "highlights": {
+ "type": "array",
+ "description": "Specify multiple accomplishments",
+ "items": {
+ "type": "string",
+ "description": "e.g. Increased profits by 20% from 2011-2012 through viral advertising"
+ }
+ }
+ }
+ }
+ },
+ "volunteer": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "additionalProperties": true,
+ "properties": {
+ "organization": {
+ "type": "string",
+ "description": "e.g. Facebook"
+ },
+ "position": {
+ "type": "string",
+ "description": "e.g. Software Engineer"
+ },
+ "url": {
+ "type": "string",
+ "description": "e.g. http://facebook.example.com",
+ "format": "uri"
+ },
+ "startDate": {
+ "$ref": "#/definitions/iso8601"
+ },
+ "endDate": {
+ "$ref": "#/definitions/iso8601"
+ },
+ "summary": {
+ "type": "string",
+ "description": "Give an overview of your responsibilities at the company"
+ },
+ "highlights": {
+ "type": "array",
+ "description": "Specify accomplishments and achievements",
+ "items": {
+ "type": "string",
+ "description": "e.g. Increased profits by 20% from 2011-2012 through viral advertising"
+ }
+ }
+ }
+ }
+ },
+ "education": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "additionalProperties": true,
+ "properties": {
+ "institution": {
+ "type": "string",
+ "description": "e.g. Massachusetts Institute of Technology"
+ },
+ "url": {
+ "type": "string",
+ "description": "e.g. http://facebook.example.com",
+ "format": "uri"
+ },
+ "area": {
+ "type": "string",
+ "description": "e.g. Arts"
+ },
+ "studyType": {
+ "type": "string",
+ "description": "e.g. Bachelor"
+ },
+ "startDate": {
+ "$ref": "#/definitions/iso8601"
+ },
+ "endDate": {
+ "$ref": "#/definitions/iso8601"
+ },
+ "score": {
+ "type": "string",
+ "description": "grade point average, e.g. 3.67/4.0"
+ },
+ "thesis": {
+ "type": "string",
+ "description": "Title of the thesis for Master's degree"
+ },
+ "dissertation": {
+ "type": "string",
+ "description": "Title of the dissertation for Doctoral degree"
+ },
+ "courses": {
+ "type": "array",
+ "description": "List notable courses/subjects",
+ "items": {
+ "type": "string",
+ "description": "e.g. H1302 - Introduction to American history"
+ }
+ }
+ }
+ }
+ },
+ "awards": {
+ "type": "array",
+ "description": "Specify any awards you have received throughout your professional career",
+ "items": {
+ "type": "object",
+ "additionalProperties": true,
+ "properties": {
+ "title": {
+ "type": "string",
+ "description": "e.g. One of the 100 greatest minds of the century"
+ },
+ "date": {
+ "$ref": "#/definitions/iso8601"
+ },
+ "awarder": {
+ "type": "string",
+ "description": "e.g. Time Magazine"
+ },
+ "summary": {
+ "type": "string",
+ "description": "e.g. Received for my work with Quantum Physics"
+ }
+ }
+ }
+ },
+ "certificates": {
+ "type": "array",
+ "description": "Specify any certificates you have received throughout your professional career",
+ "items": {
+ "type": "object",
+ "additionalProperties": true,
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "e.g. Certified Kubernetes Administrator"
+ },
+ "date": {
+ "type": "string",
+ "description": "e.g. 1989-06-12",
+ "format": "date"
+ },
+ "url": {
+ "type": "string",
+ "description": "e.g. http://example.com",
+ "format": "uri"
+ },
+ "issuer": {
+ "type": "string",
+ "description": "e.g. CNCF"
+ }
+ }
+ }
+ },
+ "publications": {
+ "type": "array",
+ "description": "Publications from both BibTeX and manual entry",
+ "items": {
+ "type": "object",
+ "additionalProperties": true,
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "Title of the publication"
+ },
+ "type": {
+ "type": "string",
+ "enum": ["journal", "conference", "other"],
+ "description": "Type of publication"
+ },
+ "url": {
+ "type": "string",
+ "format": "uri",
+ "description": "DOI or URL to the publication"
+ },
+ "formattedHTML": {
+ "type": "string",
+ "description": "Pre-formatted citation HTML (from BibTeX or manual)"
+ },
+ "source": {
+ "type": "string",
+ "enum": ["bibtex", "manual"],
+ "description": "Source of this publication entry"
+ },
+ "authors": {
+ "type": "array",
+ "items": { "type": "string" },
+ "description": "List of authors"
+ },
+ "publisher": {
+ "type": "string",
+ "description": "Journal/conference name or publisher"
+ },
+ "releaseDate": {
+ "type": "string",
+ "description": "Publication date"
+ },
+ "citationKey": {
+ "type": "string",
+ "description": "BibTeX citation key (for BibTeX entries)"
+ },
+ "bibtexEntry": {
+ "type": "object",
+ "description": "Full BibTeX entry data (for BibTeX entries)"
+ }
+ },
+ "required": ["name", "type", "formattedHTML", "source"]
+ }
+ },
+ "skills": {
+ "type": "array",
+ "description": "List out your professional skill-set",
+ "items": {
+ "type": "object",
+ "additionalProperties": true,
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "e.g. Web Development"
+ },
+ "level": {
+ "type": "string",
+ "description": "e.g. Master"
+ },
+ "summary": {
+ "type": "string",
+ "description": "e.g. Expert level proficiency in web development with extensive knowledge and experience in multiple programming languages, frameworks and web technologies."
+ },
+ "keywords": {
+ "type": "array",
+ "description": "List some keywords pertaining to this skill",
+ "items": {
+ "type": "string",
+ "description": "e.g. HTML"
+ }
+ }
+ }
+ }
+ },
+ "languages": {
+ "type": "array",
+ "description": "List any other languages you speak",
+ "items": {
+ "type": "object",
+ "additionalProperties": true,
+ "properties": {
+ "language": {
+ "type": "string",
+ "description": "e.g. English, Spanish"
+ },
+ "fluency": {
+ "type": "string",
+ "description": "e.g. Fluent, Beginner"
+ },
+ "summary": {
+ "type": "string",
+ "description": "e.g. Proficient in English with a pretty good level of speaking, writing and comprehension abilities."
+ }
+ }
+ }
+ },
+ "interests": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "additionalProperties": true,
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "e.g. Philosophy"
+ },
+ "keywords": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "description": "e.g. Friedrich Nietzsche"
+ }
+ }
+ }
+ }
+ },
+ "references": {
+ "type": "array",
+ "description": "List references you have received",
+ "items": {
+ "type": "object",
+ "additionalProperties": true,
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "e.g. Timothy Cook"
+ },
+ "reference": {
+ "type": "string",
+ "description": "e.g. Joe blogs was a great employee, who turned up to work at least once a week. He exceeded my expectations when it came to doing nothing."
+ }
+ }
+ }
+ },
+ "projects": {
+ "type": "array",
+ "description": "Specify career projects and research grants",
+ "items": {
+ "type": "object",
+ "additionalProperties": true,
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "e.g. The World Wide Web"
+ },
+ "description": {
+ "type": "string",
+ "description": "Short summary of project. e.g. Collated works of 2017."
+ },
+ "highlights": {
+ "type": "array",
+ "description": "Specify multiple features",
+ "items": {
+ "type": "string",
+ "description": "e.g. Directs you close but not quite there"
+ }
+ },
+ "keywords": {
+ "type": "array",
+ "description": "Specify special elements involved",
+ "items": {
+ "type": "string",
+ "description": "e.g. AngularJS"
+ }
+ },
+ "startDate": {
+ "type": "string",
+ "description": "e.g. 2024-02-01"
+ },
+ "endDate": {
+ "type": "string",
+ "description": "e.g. 2024-02-01"
+ },
+ "url": {
+ "type": "string",
+ "format": "uri",
+ "description": "e.g. http://www.computer.org/csdl/mags/co/1996/10/rx069-abs.html"
+ },
+ "roles": {
+ "type": "array",
+ "description": "Specify your role on this project or in company",
+ "items": {
+ "type": "string",
+ "description": "e.g. Team Lead, Speaker, Writer"
+ }
+ },
+ "entity": {
+ "type": "string",
+ "description": "Specify the relevant company/entity affiliations e.g. 'greenpeace', 'corporationXYZ'"
+ },
+ "type": {
+ "type": "string",
+ "description": " e.g. 'volunteering', 'presentation', 'talk', 'application', 'conference'"
+ },
+ "projectType": {
+ "type": "string",
+ "description": "Type of project",
+ "enum": ["research", "software", "education", "grant", "other"]
+ },
+ "fundingDetails": {
+ "type": "object",
+ "description": "Funding information if project is a grant",
+ "properties": {
+ "role": {
+ "type": "string",
+ "description": "e.g. PI, co-PI"
+ },
+ "fundingAgency": {
+ "type": "string",
+ "description": "e.g. National Science Foundation"
+ },
+ "amount": {
+ "type": "string",
+ "description": "e.g. $500,000"
+ },
+ "status": {
+ "type": "string",
+ "enum": ["active", "completed", "pending"]
+ }
+ }
+ }
+ }
+ }
+ },
+ "sideProjects": {
+ "type": "array",
+ "description": "Specify side projects",
+ "items": {
+ "type": "object",
+ "additionalProperties": true,
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "e.g. The World Wide Web"
+ },
+ "url": {
+ "type": "string",
+ "format": "uri",
+ "description": "e.g. http://www.computer.org/csdl/mags/co/1996/10/rx069-abs.html"
+ },
+ "description": {
+ "type": "string",
+ "description": "Short summary of project. e.g. Collated works of 2017."
+ },
+ "keywords": {
+ "type": "array",
+ "description": "Specify special elements involved",
+ "items": {
+ "type": "string",
+ "description": "e.g. AngularJS"
+ }
+ },
+ "startDate": {
+ "type": "string",
+ "description": "e.g. 2024-02-01"
+ },
+ "endDate": {
+ "type": "string",
+ "description": "e.g. 2024-02-01"
+ }
+ }
+ }
+ },
+ "meta": {
+ "type": "object",
+ "description": "The schema version and any other tooling configuration lives here",
+ "additionalProperties": true,
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "The name of this JSONCV data, will be used as part of the filename when downloading."
+ },
+ "canonical": {
+ "type": "string",
+ "description": "URL (as per RFC 3986) to latest version of this document",
+ "format": "uri"
+ },
+ "version": {
+ "type": "string",
+ "description": "A version field which follows semver - e.g. v1.0.0"
+ },
+ "lastModified": {
+ "type": "string",
+ "description": "Using ISO 8601 with YYYY-MM-DDThh:mm:ss"
+ },
+ "colorPrimary": {
+ "type": "string",
+ "description": "Primary color in hex format (e.g. #2A3FFB) to be used for styling the CV",
+ "pattern": "^#[0-9A-Fa-f]{6}$"
+ },
+ "colorSecondary": {
+ "type": "string",
+ "description": "Secondary color in hex format (e.g. #777777) to be used for dimmed text in the CV",
+ "pattern": "^#[0-9A-Fa-f]{6}$"
+ },
+ "citationStyles": {
+ "type": "object",
+ "description": "Styles for highlighting names in citations",
+ "properties": {
+ "owner": {
+ "type": "string",
+ "description": "Style for owner's name in citations",
+ "enum": ["bold", "asterisk", "plus"]
+ },
+ "mentee": {
+ "type": "string",
+ "description": "Style for mentee names in citations",
+ "enum": ["bold", "asterisk", "plus"]
+ }
+ },
+ "default": {
+ "owner": "bold",
+ "mentee": "plus"
+ }
+ },
+ "pageSize": {
+ "type": "string",
+ "description": "Page size for the CV (A4 or letter)",
+ "enum": ["A4", "letter"],
+ "default": "A4"
+ },
+ "theme": {
+ "type": "string",
+ "description": "Theme for the CV",
+ "enum": ["reorx", "cuiv"],
+ "default": "reorx"
+ }
+ }
+ },
+ "teaching": {
+ "type": "array",
+ "description": "Courses taught and teaching experience",
+ "items": {
+ "type": "object",
+ "properties": {
+ "courseCode": {
+ "type": "string",
+ "description": "e.g. ECE 4153"
+ },
+ "courseName": {
+ "type": "string",
+ "description": "e.g. Power System Analysis and Design"
+ },
+ "description": {
+ "type": "string",
+ "description": "Course description and topics covered"
+ },
+ "institution": {
+ "type": "string"
+ },
+ "terms": {
+ "type": "array",
+ "items": {
+ "type": "string",
+ "description": "e.g. F'22, S'24"
+ }
+ }
+ }
+ }
+ },
+ "mentoring": {
+ "type": "object",
+ "description": "Students mentored/advised by category",
+ "properties": {
+ "current_students": {
+ "type": "array",
+ "description": "Current students at my institution",
+ "items": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "level": {
+ "type": "string",
+ "enum": ["Ph.D.", "M.S.", "Undergraduate"],
+ "description": "Academic level"
+ },
+ "startDate": {
+ "type": "string",
+ "description": "Start date in any format"
+ },
+ "url": {
+ "type": "string",
+ "description": "URL to student's website or profile",
+ "format": "uri"
+ },
+ "dissertation": {
+ "type": "string",
+ "description": "Dissertation topic or title (for PhD students)"
+ },
+ "type": {
+ "type": "string",
+ "enum": ["Thesis", "Course-based"],
+ "description": "Type of Master's program (for MS students)"
+ },
+ "thesis_title": {
+ "type": "string",
+ "description": "Thesis title (for thesis-based MS students)"
+ },
+ "program": {
+ "type": "string",
+ "description": "Program name (for undergraduate students)"
+ },
+ "period": {
+ "type": "string",
+ "description": "Academic period (for undergraduate students)"
+ }
+ },
+ "required": ["name", "level"]
+ }
+ },
+ "committee_service": {
+ "type": "array",
+ "description": "PhD committee service at other institutions",
+ "items": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "institution": {
+ "type": "string",
+ "description": "Institution where the student is/was enrolled"
+ },
+ "dissertation": {
+ "type": "string",
+ "description": "Dissertation title"
+ },
+ "status": {
+ "type": "string",
+ "enum": ["Current", "Completed"],
+ "description": "Current status of the student"
+ },
+ "completionDate": {
+ "type": "string",
+ "description": "Date of completion in any format"
+ }
+ },
+ "required": ["name", "institution", "dissertation", "status"]
+ }
+ },
+ "past_students": {
+ "type": "array",
+ "description": "Past students who have completed their degrees",
+ "items": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "level": {
+ "type": "string",
+ "enum": ["Ph.D.", "M.S.", "Undergraduate"],
+ "description": "Academic level"
+ },
+ "institution": {
+ "type": "string",
+ "description": "Institution where the student was enrolled"
+ },
+ "startDate": {
+ "type": "string",
+ "description": "Start date in any format"
+ },
+ "endDate": {
+ "type": "string",
+ "description": "End date in any format"
+ },
+ "url": {
+ "type": "string",
+ "description": "URL to student's website or profile",
+ "format": "uri"
+ },
+ "dissertation": {
+ "type": "string",
+ "description": "Dissertation title (for PhD students)"
+ },
+ "type": {
+ "type": "string",
+ "enum": ["Thesis", "Course-based"],
+ "description": "Type of Master's program (for MS students)"
+ },
+ "thesis_title": {
+ "type": "string",
+ "description": "Thesis title (for thesis-based MS students)"
+ },
+ "program": {
+ "type": "string",
+ "description": "Program name (for undergraduate students)"
+ },
+ "period": {
+ "type": "string",
+ "description": "Academic period (for undergraduate students)"
+ },
+ "placement": {
+ "type": "object",
+ "properties": {
+ "position": {
+ "type": "string",
+ "description": "Job title or position"
+ },
+ "organization": {
+ "type": "string",
+ "description": "Company or institution name"
+ },
+ "location": {
+ "type": "string",
+ "description": "Location of the position"
+ }
+ }
+ }
+ },
+ "required": ["name", "level", "institution"]
+ }
+ }
+ }
+ },
+ "software": {
+ "type": "array",
+ "description": "Software projects and tools developed",
+ "items": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string"
+ },
+ "url": {
+ "type": "string",
+ "format": "uri"
+ },
+ "metrics": {
+ "type": "object",
+ "properties": {
+ "stars": {
+ "type": "integer"
+ },
+ "forks": {
+ "type": "integer"
+ }
+ }
+ }
+ }
+ }
+ },
+ "researchAreas": {
+ "type": "array",
+ "description": "Research areas and contributions",
+ "items": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "Name of the research area"
+ },
+ "contributions": {
+ "type": "array",
+ "description": "List of contributions in this area",
+ "items": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "services": {
+ "type": "object",
+ "description": "Professional service activities",
+ "properties": {
+ "editorialBoards": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "role": {
+ "type": "string",
+ "description": "e.g. Associate Editor, Guest Editor"
+ },
+ "journal": {
+ "type": "string"
+ },
+ "period": {
+ "type": "string",
+ "description": "e.g. 2012-Present"
+ },
+ "details": {
+ "type": "string",
+ "description": "Additional details about the role"
+ }
+ }
+ }
+ },
+ "societyServices": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "role": {
+ "type": "string"
+ },
+ "organization": {
+ "type": "string"
+ },
+ "period": {
+ "type": "string"
+ }
+ }
+ }
+ },
+ "reviewServices": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "type": {
+ "type": "string",
+ "description": "e.g. Proposal Review, Journal Review"
+ },
+ "organization": {
+ "type": "string"
+ },
+ "year": {
+ "type": "string"
+ },
+ "details": {
+ "type": "string"
+ }
+ }
+ }
+ },
+ "departmental": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "role": {
+ "type": "string"
+ },
+ "committee": {
+ "type": "string"
+ },
+ "period": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ }
+ },
+ "presentations": {
+ "type": "object",
+ "description": "Presentations and talks",
+ "properties": {
+ "invitedTalks": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "venue": {
+ "type": "string"
+ },
+ "type": {
+ "type": "string",
+ "description": "e.g. Webinar, Seminar"
+ },
+ "date": {
+ "type": "string"
+ },
+ "title": {
+ "type": "string"
+ },
+ "url": {
+ "type": "string",
+ "description": "URL to presentation materials",
+ "format": "uri"
+ }
+ }
+ }
+ },
+ "conferences": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "title": {
+ "type": "string"
+ },
+ "venue": {
+ "type": "string"
+ },
+ "location": {
+ "type": "string"
+ },
+ "date": {
+ "type": "string"
+ },
+ "type": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string",
+ "description": "Description of the presentation content and significance"
+ },
+ "url": {
+ "type": "string",
+ "description": "URL to presentation materials",
+ "format": "uri"
+ }
+ }
+ }
+ },
+ "outreach": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "activity": {
+ "type": "string"
+ },
+ "date": {
+ "type": "string"
+ },
+ "description": {
+ "type": "string",
+ "description": "Detailed description of the outreach activity and its impact"
+ },
+ "url": {
+ "type": "string",
+ "description": "URL to related materials",
+ "format": "uri"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "title": "CV Schema",
+ "type": "object"
+}
\ No newline at end of file
diff --git a/src/editor/index.html b/src/editor/index.html
index 50ab87a..1f6ce1c 100644
--- a/src/editor/index.html
+++ b/src/editor/index.html
@@ -26,13 +26,29 @@
#aaaaaa
+
+ Theme
+
+
Preview/JSON
Download JSON
Download HTML
PDF?
New Data
Upload Data
-
+
+
+
+
BibTeX
+
Upload
+
Download
+
+ Replace
+ Merge
+
+
+
+
Load Sample
diff --git a/src/editor/main.js b/src/editor/main.js
index a001ae9..de2f052 100644
--- a/src/editor/main.js
+++ b/src/editor/main.js
@@ -1,18 +1,21 @@
-import 'iconify-icon'; // import only
-
+// Core dependencies first
+import { JSONEditor } from '@json-editor/json-editor/dist/jsoneditor';
import $ from 'cash-dom';
import dayjs from 'dayjs';
import objectPath from 'object-path';
-import { JSONEditor } from '@json-editor/json-editor/dist/jsoneditor';
-
-import * as sampleModule from '../../sample.cv.json';
+// Local imports
+import * as sampleModule from '../../sample/sample.cv.prof.json';
import * as jsoncvSchemaModule from '../../schema/jsoncv.schema.json';
+import { processBibTeX, updatePublications, getLastBibTeX } from '../lib/bibtex';
import {
getCVData,
getPrimaryColor,
saveCVJSON,
savePrimaryColor,
+ savePageSize,
+ getTheme,
+ saveTheme,
} from '../lib/store';
import {
createElement,
@@ -22,139 +25,222 @@ import {
traverseDownObject,
} from '../lib/utils';
import { getCVTitle } from '../themes/data';
+import { getAvailableThemes, getThemeDefaultColor } from '../themes';
import { registerIconLib } from './je-iconlib';
import { registerTheme } from './je-theme';
-const propertiesInOrder = ['basics', 'education', 'work', 'projects', 'sideProjects', 'skills', 'languages', 'interests', 'references', 'awards', 'publications', 'volunteer', 'certificates', 'meta']
-const basicsPropertiesInOrder = ['name', 'label', 'email', 'phone', 'url', 'summary', 'image', 'location', 'profiles']
+// Load iconify last as it's optional for UI
+import 'iconify-icon';
// toc elements
const elToc = document.querySelector('.editor-toc')
-const tocUl = createElement('ul', {
- parent: elToc
-})
-const basicsUl = createElement('ul', {
- parent: tocUl
-})
+const tocUl = createElement('ul', { parent: elToc })
+const basicsUl = createElement('ul', { parent: tocUl })
+// init data
+let data = getCVData()
+if (!data) data = sampleModule.default
-// copy the object to remove the readonly restriction on module
-const jsoncvSchema = {...jsoncvSchemaModule.default}
+// Initialize editor with schema and data
+async function initializeEditor() {
+ // Get schema (remote or local)
+ let schema
+ if (data.$schema) {
+ try {
+ const response = await fetch(data.$schema)
+ schema = await response.json()
+ } catch (error) {
+ console.error('Error fetching schema:', error)
+ schema = {...jsoncvSchemaModule.default}
+ }
+ } else {
+ schema = {...jsoncvSchemaModule.default}
+ }
-// add propertyOrder to schema, and add links to toc
-propertiesInOrder.forEach((name, index) => {
- jsoncvSchema.properties[name].propertyOrder = index
+ // Validate schema structure
+ if (!schema || !schema.properties) {
+ schema = {...jsoncvSchemaModule.default}
+ }
- const li = createElement('li', {parent: tocUl})
- createElement('a', {
- text: name,
- attrs: {
- href: `#root.${name}`
- },
- parent: li,
+ // Add property order and build TOC
+ const excludeFromTOC = ['$schema', 'sideProjects', 'languages', 'interests']
+
+ Object.keys(schema.properties).forEach((name, index) => {
+ // Add order
+ schema.properties[name].propertyOrder = index
+
+ // Skip excluded sections in TOC
+ if (excludeFromTOC.includes(name)) {
+ return
+ }
+
+ // Create TOC entry
+ const li = createElement('li', { parent: tocUl })
+ createElement('a', {
+ text: name,
+ attrs: { href: `#root.${name}` },
+ parent: li,
+ })
+
+ // Special handling for basics section
+ if (name === 'basics' && schema.properties[name].properties) {
+ // only add location and profiles to basics toc
+ ['location', 'profiles'].forEach(subName => {
+ if (schema.properties[name].properties[subName]) {
+ const subLi = createElement('li', { parent: basicsUl })
+ createElement('a', {
+ text: subName,
+ attrs: { href: `#root.basics.${subName}` },
+ parent: subLi,
+ })
+ }
+ })
+ li.appendChild(basicsUl)
+ }
})
- if (name === 'basics') {
- li.appendChild(basicsUl)
- }
-})
-basicsPropertiesInOrder.forEach((name, index) => {
- jsoncvSchema.properties.basics.properties[name].propertyOrder = index
- // only add location and profiles to basics toc
- if (!['location', 'profiles'].includes(name)) return
- const li = createElement('li', {parent: basicsUl})
- createElement('a', {
- text: name,
- attrs: {
- href: `#root.basics.${name}`
- },
- parent: li,
+ // Add headerTemplate for arrays
+ traverseDownObject(schema, (key, obj) => {
+ let noun = key
+ if (noun.endsWith('s')) noun = noun.slice(0, -1)
+ if (obj.type === 'array' && obj.items) {
+ obj.items.headerTemplate = `${noun} {{i1}}`
+ }
})
-})
-// add headerTemplate for each type:array in schema
-traverseDownObject(jsoncvSchema, (key, obj) => {
- let noun = key
- if (noun.endsWith('s')) noun = noun.slice(0, -1)
- if (obj.type === 'array' && obj.items) {
- obj.items.headerTemplate = `${noun} {{i1}}`
- }
-})
+ // Add format to schema
+ const keyFormatMap = {
+ // Basic fields
+ 'basics.properties.summary': 'textarea',
-// add format to schema
-const keyFormatMap = {
- 'basics.properties.summary': 'textarea',
- 'work.items.properties.description': 'textarea',
- 'work.items.properties.summary': 'textarea',
- 'work.items.properties.highlights.items': 'textarea',
- 'projects.items.properties.description': 'textarea',
- 'projects.items.properties.highlights.items': 'textarea',
- 'sideProjects.items.properties.description': 'textarea',
- 'skills.items.properties.summary': 'textarea',
- 'languages.items.properties.summary': 'textarea',
- 'references.items.properties.reference': 'textarea',
- 'awards.items.properties.summary': 'textarea',
- 'publications.items.properties.summary': 'textarea',
- 'volunteer.items.properties.summary': 'textarea',
- 'volunteer.items.properties.highlights.items': 'textarea',
-}
-for (const [key, format] of Object.entries(keyFormatMap)) {
- objectPath.get(jsoncvSchema.properties, key).format = format
-}
+ // Work and projects
+ 'work.items.properties': {
+ 'description': 'textarea',
+ 'summary': 'textarea',
+ 'highlights.items': 'textarea'
+ },
+ 'projects.items.properties': {
+ 'description': 'textarea',
+ 'highlights.items': 'textarea'
+ },
+ 'sideProjects.items.properties.description': 'textarea',
+
+ // Skills and qualifications
+ 'skills.items.properties.summary': 'textarea',
+ 'languages.items.properties.summary': 'textarea',
+ 'references.items.properties.reference': 'textarea',
+
+ // Awards and recognition
+ 'awards.items.properties.summary': 'textarea',
+ 'publications.items.properties.summary': 'textarea',
+ 'volunteer.items.properties': {
+ 'summary': 'textarea',
+ 'highlights.items': 'textarea'
+ },
-// change schema title
-jsoncvSchema.title = 'CV Schema'
+ // Academic fields
+ 'researchAreas.items.properties': {
+ 'description': 'textarea',
+ 'contributions.items': 'textarea'
+ },
+ 'academicAppointments.items.properties.summary': 'textarea',
+ 'teaching.items.properties.description': 'textarea',
+ 'grants.items.properties.summary': 'textarea',
+ 'mentoring.items.properties.summary': 'textarea',
+ 'professionalServices.items.properties.details': 'textarea',
+ 'presentations.items.properties.description': 'textarea',
+ 'software.items.properties.description': 'textarea'
+ }
-// change some descriptions
-jsoncvSchema.properties.meta.properties.lastModified.description += '. This will be automatically updated when downloading.'
+ // Process nested format mappings
+ for (const [key, value] of Object.entries(keyFormatMap)) {
+ if (typeof value === 'string') {
+ // Direct format mapping
+ const property = objectPath.get(schema.properties, key)
+ if (property) {
+ property.format = value
+ }
+ } else {
+ // Nested format mappings
+ for (const [subKey, format] of Object.entries(value)) {
+ const fullKey = `${key}.${subKey}`
+ const property = objectPath.get(schema.properties, fullKey)
+ if (property) {
+ property.format = format
+ }
+ }
+ }
+ }
+ // Set schema title and update descriptions
+ schema.title = 'CV Schema'
+ if (schema.properties.meta?.properties?.lastModified) {
+ schema.properties.meta.properties.lastModified.description += '. This will be automatically updated when downloading.'
+ }
-// init data
-let data = getCVData()
-if (!data) data = sampleModule.default
+ // Initialize editor
+ registerTheme(JSONEditor)
+ registerIconLib(JSONEditor)
+ const elEditorContainer = document.querySelector('.editor-container')
+ const editor = new JSONEditor(elEditorContainer, {
+ schema: schema,
+ theme: 'mytheme',
+ iconlib: 'myiconlib',
+ disable_array_delete_all_rows: true,
+ no_additional_properties: true,
+ startval: data,
+ });
+
+ return editor;
+}
-// initialize editor
-registerTheme(JSONEditor)
-registerIconLib(JSONEditor)
-const elEditorContainer = document.querySelector('.editor-container')
-const editor = new JSONEditor(elEditorContainer, {
- schema: jsoncvSchema,
- theme: 'mytheme',
- iconlib: 'myiconlib',
- disable_array_delete_all_rows: true,
- no_additional_properties: true,
- startval: data,
+// Initialize the editor
+let editor;
+initializeEditor().then(e => {
+ editor = e;
+ // Add ready handler
+ editor.on('ready',() => {
+ // add anchor to each schema element
+ document.querySelectorAll('[data-schemapath]').forEach(el => {
+ const schemapath = el.getAttribute('data-schemapath')
+ el.id = schemapath
+ })
+
+ // Initialize color picker from meta or localStorage
+ const initialData = editor.getValue()
+ const primaryColor = initialData.meta?.colorPrimary || getPrimaryColor()
+ $colorValue.text(primaryColor)
+ $inputColorPicker.val(primaryColor)
+
+ // Set initial theme from meta or storage
+ const initialTheme = initialData.meta?.theme || getTheme()
+ $themePicker.val(initialTheme)
+ saveTheme(initialTheme) // Ensure storage is synced
+ });
+
+ // Add change handler
+ editor.on('change', () => {
+ const data = editor.getValue()
+ const json = JSON.stringify(data, null, 2)
+ $outputJSON.text(json)
+
+ // save to localstorage
+ saveCVJSON(json)
+
+ // sync page size with localStorage
+ const pageSize = data.meta?.pageSize || 'A4'
+ savePageSize(pageSize)
+ });
});
-editor.on('ready',() => {
- // add anchor to each schema element
- document.querySelectorAll('[data-schemapath]').forEach(el => {
- const schemapath = el.getAttribute('data-schemapath')
- el.id = schemapath
- })
-})
function getEditorData() {
- const data = editor.getValue()
- return {
- data,
- json: JSON.stringify(data, null, 2),
- }
+ return editor.getValue()
}
const $outputJSON = $('.output-json')
const $outputHTML = $('.output-html')
const outputHTMLIframe = $outputHTML.get(0)
-// listen to change
-editor.on('change', () => {
- console.log('on editor change')
- const {json} = getEditorData()
- $outputJSON.text(json)
-
- // save to localstorage
- saveCVJSON(json)
-})
-
// actions
const $btnTogglePreview = $('#fn-toggle-preview')
const $btnNewData = $('#fn-new-data')
@@ -256,11 +342,93 @@ $btnPrintPreview.on('click', () => {
$inputColorPicker.on('change', (e) => {
const color = e.target.value
- console.log('color', color)
- $colorValue.text(color)
+ console.log('color changed to:', color)
+
+ // Update both meta and localStorage
+ const editorData = editor.getValue()
+ if (!editorData.meta) editorData.meta = {}
+ editorData.meta.colorPrimary = color
+ editor.setValue(editorData)
savePrimaryColor(color)
+ $colorValue.text(color)
})
-const primaryColor = getPrimaryColor()
-$colorValue.text(primaryColor)
-$inputColorPicker.val(primaryColor)
+// Initialize theme picker
+const $themePicker = $('#fn-theme-picker')
+const themes = getAvailableThemes()
+themes.forEach(theme => {
+ const option = createElement('option', {
+ text: theme,
+ attrs: { value: theme }
+ })
+ $themePicker.append(option)
+})
+
+// Theme colors
+const themeColors = {
+ reorx: '#aaaaaa',
+ cuiv: '#cc0000'
+}
+
+// Handle theme changes
+$themePicker.on('change', (e) => {
+ const selectedTheme = e.target.value
+ console.log('theme changed to:', selectedTheme)
+
+ // Update both theme and color in one operation
+ const editorData = editor.getValue()
+ if (!editorData.meta) editorData.meta = {}
+ editorData.meta.theme = selectedTheme
+ editorData.meta.colorPrimary = getThemeDefaultColor(selectedTheme)
+
+ // Update editor and storage
+ editor.setValue(editorData) // triggers preview refresh
+ saveTheme(selectedTheme)
+ savePrimaryColor(editorData.meta.colorPrimary)
+
+ // Update color picker UI
+ $colorValue.text(editorData.meta.colorPrimary)
+ $inputColorPicker.val(editorData.meta.colorPrimary)
+})
+
+// BibTeX handling
+const $btnUploadBibTeX = $('#fn-upload-bibtex')
+const $btnDownloadBibTeX = $('#fn-download-bibtex')
+const $inputUploadBibTeX = $('input[name=upload-bibtex]')
+
+$btnUploadBibTeX.on('click', () => {
+ $inputUploadBibTeX.trigger('click')
+})
+
+$inputUploadBibTeX.on('change', async () => {
+ const files = $inputUploadBibTeX.get(0).files
+ if (files.length === 0) return
+
+ const reader = new FileReader()
+ reader.onload = async (e) => {
+ try {
+ const bibtexStr = e.target.result
+ const currentData = editor.getValue()
+ const publications = await processBibTeX(bibtexStr, currentData)
+
+ // Get current mode from radio buttons
+ const shouldReplace = $('input[name="bibtex-mode"]:checked').val() === 'replace'
+ const newData = updatePublications(currentData, publications, shouldReplace)
+ editor.setValue(newData)
+ } catch (e) {
+ console.error('Error processing BibTeX:', e)
+ alert('Error processing BibTeX file. Please check the console for details.')
+ }
+ }
+
+ reader.readAsText(files[0])
+})
+
+$btnDownloadBibTeX.on('click', () => {
+ const bibtexStr = getLastBibTeX()
+ if (!bibtexStr) {
+ alert('No BibTeX data available. Please upload a BibTeX file first.')
+ return
+ }
+ downloadContent('references.bib', bibtexStr)
+})
diff --git a/src/lib/bibtex.js b/src/lib/bibtex.js
new file mode 100644
index 0000000..27f4a56
--- /dev/null
+++ b/src/lib/bibtex.js
@@ -0,0 +1,167 @@
+import { Cite } from '@citation-js/core'
+import '@citation-js/plugin-bibtex'
+import '@citation-js/plugin-csl'
+import { plugins } from '@citation-js/core'
+
+// Fetch and register IEEE CSL template
+fetch('https://raw.githubusercontent.com/citation-style-language/styles/master/ieee.csl')
+ .then(response => response.text())
+ .then(template => {
+ plugins.config.get('@csl').templates.add('ieee', template)
+ console.log('[Debug] Successfully registered IEEE CSL template')
+ })
+ .catch(error => {
+ console.error('[Error] Failed to fetch IEEE CSL template:', error)
+ })
+
+/**
+ * Map BibTeX entry types to CV publication types
+ */
+const PUBLICATION_TYPE_MAP = {
+ // Journal types
+ 'article': 'journal',
+ 'article-journal': 'journal',
+ 'journal-article': 'journal',
+
+ // Conference types
+ 'paper-conference': 'conference',
+ 'inproceedings': 'conference',
+ 'conference': 'conference',
+ 'conference-paper': 'conference',
+ 'proceedings-article': 'conference',
+
+ // Keep other types mapped to 'other' by default
+}
+
+// Store the last uploaded BibTeX
+let lastBibTeX = null
+
+/**
+ * Process BibTeX string and return publications in JSONCV format
+ */
+export function processBibTeX(bibtexStr, cvData, shouldReplace = false) {
+ // Store raw BibTeX
+ lastBibTeX = bibtexStr
+
+ try {
+ // Parse BibTeX
+ const cite = new Cite(bibtexStr)
+ console.log('[Debug] Total entries in BibTeX:', cite.data.length)
+
+ // Format all entries together to get correct numbering
+ const formattedHTML = cite.format('bibliography', {
+ format: 'html',
+ template: 'ieee',
+ lang: 'en-US'
+ })
+
+ // Clean up the HTML structure while preserving content
+ const processedHTML = formattedHTML
+ .replace(/\r?\n|\r/g, '') // Remove all newlines
+ .replace(/>\s+<') // Remove whitespace between tags
+ .replace(/\s+/g, ' ') // Replace multiple spaces with single space
+ .replace(/, doi: .*?(?=<\/div>)/g, '') // Remove DOI from citation text
+ .trim() // Remove leading/trailing whitespace
+
+ // Extract individual citations from the processed HTML
+ const citationDivs = processedHTML.match(/.*?<\/div>/g) || []
+
+ // Process each entry with its formatted citation
+ const publications = cite.data.map((entry, index) => {
+ console.log(`[Debug] Entry ${index} type:`, entry.type)
+
+ // Format individual citation
+ const singleCite = new Cite(entry)
+ const formattedHTML = singleCite.format('bibliography', {
+ format: 'html',
+ template: 'ieee',
+ lang: 'en-US'
+ })
+
+ // Clean up the HTML structure
+ const processedHTML = formattedHTML
+ .replace(/\r?\n|\r/g, '') // Remove all newlines
+ .replace(/>\s+<') // Remove whitespace between tags
+ .replace(/\s+/g, ' ') // Replace multiple spaces with single space
+ .replace(/, doi: .*?(?=<\/div>)/g, '') // Remove DOI from citation text
+ .trim() // Remove leading/trailing whitespace
+
+ const pubType = PUBLICATION_TYPE_MAP[entry.type] || 'other'
+ const pub = {
+ name: entry.title,
+ publisher: entry['container-title'] || entry.journal || '',
+ releaseDate: entry.issued?.['date-parts']?.[0]?.[0]?.toString() || entry.year?.toString() || '',
+ url: entry.DOI ? `https://doi.org/${entry.DOI}` : entry.URL || '',
+ type: pubType,
+ formattedHTML: `
${processedHTML}
`,
+ citationKey: entry.id,
+ bibtexEntry: entry,
+ source: 'bibtex'
+ }
+ return pub
+ })
+
+ // Create a new CV data object with updated publications
+ const newCvData = { ...cvData }
+
+ if (shouldReplace) {
+ // In replace mode, use only BibTeX publications
+ newCvData.publications = publications
+ } else {
+ // In merge mode, combine with existing publications
+ newCvData.publications = [...(cvData.publications || []), ...publications]
+ }
+
+ // Sort all publications by release date (newest first)
+ newCvData.publications.sort((a, b) => {
+ const dateA = a.releaseDate ? new Date(a.releaseDate) : new Date(0);
+ const dateB = b.releaseDate ? new Date(b.releaseDate) : new Date(0);
+ return dateB - dateA;
+ });
+
+ console.log('[Debug] CV publications after update:', {
+ mode: shouldReplace ? 'replace' : 'merge',
+ total: newCvData.publications.length,
+ fromBibtex: publications.length,
+ })
+ console.log(newCvData)
+
+ return newCvData
+ } catch (error) {
+ console.error('[Error] Failed to process BibTeX:', error)
+ return cvData
+ }
+}
+
+/**
+ * Get the last uploaded BibTeX data
+ */
+export function getLastBibTeX() {
+ return lastBibTeX
+}
+
+/**
+ * Parse stringified publication if needed
+ */
+function _parsePublication(pub) {
+ if (typeof pub === 'string') {
+ try {
+ return JSON.parse(pub)
+ } catch (e) {
+ console.error('Failed to parse publication:', e)
+ return pub
+ }
+ }
+ return pub
+}
+
+/**
+ * Update publications in CV data (for backward compatibility)
+ */
+export function updatePublications(cvData, publications, shouldReplace = false) {
+ // Just pass through to processBibTeX if we have BibTeX data
+ if (lastBibTeX) {
+ return processBibTeX(lastBibTeX, cvData, shouldReplace)
+ }
+ return cvData
+}
\ No newline at end of file
diff --git a/src/lib/date.js b/src/lib/date.js
index a0b4068..c67df5f 100644
--- a/src/lib/date.js
+++ b/src/lib/date.js
@@ -1,5 +1,8 @@
import dayjs from 'dayjs';
export function reformatDate(dateStr, toFormat, fromFormat = 'YYYY-MM-DD') {
+ if (!dateStr) return ''
+ const lower = dateStr.toLowerCase()
+ if (lower === 'present' || lower === 'now') return 'Present'
return dayjs(dateStr).format(toFormat)
}
diff --git a/src/lib/store.js b/src/lib/store.js
index 90a7c0f..76ecffd 100644
--- a/src/lib/store.js
+++ b/src/lib/store.js
@@ -1,35 +1,44 @@
-export const storeKeys = {
- cvJSON: 'cvJSON',
- cvSavedTime: 'cvSavedTime',
- primaryColor: 'primary-color',
-}
-
-const defaultPrimaryColor = '#2A3FFB'
-
-function updateSavedTime() {
- localStorage.setItem(storeKeys.cvSavedTime, Date.now())
-}
+const storeKeyCV = 'cv-data'
+const storeKeyCVSavedTime = 'cv-data-saved-time'
+const storeKeyPrimaryColor = 'primary-color'
+const storeKeyPageSize = 'page-size'
+const storeKeyTheme = 'theme'
-export function saveCVJSON(str) {
- localStorage.setItem(storeKeys.cvJSON, str)
- updateSavedTime()
+export function saveCVJSON(json) {
+ localStorage.setItem(storeKeyCV, json)
+ localStorage.setItem(storeKeyCVSavedTime, Date.now().toString())
}
export function getCVData() {
- const v = localStorage.getItem(storeKeys.cvJSON)
- if (!v) return
- return JSON.parse(v)
+ const json = localStorage.getItem(storeKeyCV)
+ if (!json) return null
+ return JSON.parse(json)
}
export function getCVSavedTime() {
- return localStorage.getItem(storeKeys.cvSavedTime)
+ return localStorage.getItem(storeKeyCVSavedTime)
}
export function savePrimaryColor(color) {
- localStorage.setItem(storeKeys.primaryColor, color)
- updateSavedTime()
+ localStorage.setItem(storeKeyPrimaryColor, color)
}
export function getPrimaryColor() {
- return localStorage.getItem(storeKeys.primaryColor) || defaultPrimaryColor
+ return localStorage.getItem(storeKeyPrimaryColor) || '#aaaaaa'
+}
+
+export function savePageSize(size) {
+ localStorage.setItem(storeKeyPageSize, size)
+}
+
+export function getPageSize() {
+ return localStorage.getItem(storeKeyPageSize) || 'A4'
+}
+
+export function saveTheme(theme) {
+ localStorage.setItem(storeKeyTheme, theme)
+}
+
+export function getTheme() {
+ return localStorage.getItem(storeKeyTheme) || 'reorx'
}
diff --git a/src/preview/main.js b/src/preview/main.js
index cbcaeb2..b392f1a 100644
--- a/src/preview/main.js
+++ b/src/preview/main.js
@@ -2,6 +2,8 @@ import {
getCVData,
getCVSavedTime,
getPrimaryColor,
+ getPageSize,
+ getTheme,
} from '../lib/store';
import { upsertStyleTag } from '../lib/utils';
import cvBaseStyle from '../scss/cv-base.css?inline';
@@ -36,9 +38,14 @@ const restoreScrollPosition = () => {
// Render CV
const data = getCVData()
if (data) {
+ // Set page size
+ const pageSize = data.meta?.pageSize || getPageSize()
+ document.documentElement.dataset.pageSize = pageSize.toLowerCase()
upsertStyleTag('base-style', cvBaseStyle)
- renderThemeOn(themeName, elCV, data, getPrimaryColor())
+ // Use theme from meta or storage
+ const theme = data.meta?.theme || getTheme()
+ renderThemeOn(theme, elCV, data, getPrimaryColor())
// change document title
document.title = getCVTitle(data)
diff --git a/src/scss/cv-base.css b/src/scss/cv-base.css
index 84fc2f5..0b828ce 100644
--- a/src/scss/cv-base.css
+++ b/src/scss/cv-base.css
@@ -1,23 +1,73 @@
/* Mandatory page setup for all themes */
+:root {
+ /* A4: 210mm x 297mm */
+ /* Letter: 215.9mm x 279.4mm */
+ --page-width: 210mm; /* default A4 */
+ --page-height: 297mm; /* default A4 */
+ --page-margin: 10mm;
+}
+
+:root[data-page-size="letter"] {
+ --page-width: 215.9mm;
+ --page-height: 279.4mm;
+}
+
@page {
- /* A4 */
- /* size: 210mm 297mm; */
- size: A4;
- margin: 10mm; /* approximately 0.4in */
+ size: var(--page-width) var(--page-height);
+ margin: var(--page-margin);
}
/* simulate print page size */
.cv-container {
- width: calc(210mm - 20mm);
- padding: 10mm;
+ width: calc(var(--page-width) - 2 * var(--page-margin));
+ min-height: calc(var(--page-height) - 2 * var(--page-margin));
+ padding: var(--page-margin);
+ margin: 0 auto; /* center the container */
+ background: white;
+ box-sizing: border-box;
+ position: relative;
+}
+
+/* Add page size indicator */
+.cv-container::before {
+ content: "A4";
+ position: fixed;
+ top: 5px;
+ right: 5px;
+ background: #f0f0f0;
+ padding: 2px 6px;
+ border-radius: 3px;
+ font-size: 12px;
+ opacity: 0.7;
+}
+
+:root[data-page-size="letter"] .cv-container::before {
+ content: "Letter";
}
+
@media print {
- /* remove the printing simulations and common decorations , let @page decide the size and margin */
- .cv-container {
+ .cv-container::before {
+ display: none;
+ }
+
+ @page {
+ size: var(--page-width) var(--page-height);
+ margin: var(--page-margin);
+ }
+
+ html, body {
+ width: var(--page-width);
+ height: var(--page-height);
margin: 0;
padding: 0;
+ }
+
+ .cv-container {
+ width: 100%;
+ min-height: 100%;
+ margin: 0;
+ padding: var(--page-margin);
border: 0;
- width: auto;
}
}
diff --git a/src/scss/editor/index.scss b/src/scss/editor/index.scss
index f6b2c4e..30e6a88 100644
--- a/src/scss/editor/index.scss
+++ b/src/scss/editor/index.scss
@@ -112,14 +112,32 @@ button {
}
}
}
+
+ .theme-picker {
+ @include vars.button-base;
+ border: 1px solid var(--grey-2);
+ border-radius: 2px;
+ padding: 3px 8px;
+ font-size: 13px;
+ display: block;
+ width: 85px;
+
+ select {
+ width: 100%;
+ margin-top: 4px;
+ padding: 2px;
+ }
+ }
}
.output-html {
// set a fixed size so that responsive styles won't be applied in the iframe
- width: 795px; // 210mm ≈ 793px
+ width: 875px; // 795px * 1.1 ≈ 875px
height: 100%;
- margin: 0; padding: 0; border: 0;
+ margin: 0; // remove auto margin since we're in a grid
+ padding: 0; border: 0;
border-right: 1px solid var(--grey-2);
+ display: block;
}
.output-json {
@@ -150,3 +168,65 @@ dialog {
background-color: transparent;
}
}
+
+.bibtex-controls {
+ margin: 5px 0;
+
+ .bibtex-mode {
+ margin-top: 4px;
+ display: flex;
+ gap: 12px;
+ justify-content: center;
+ font-size: 0.9em;
+
+ label {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ cursor: pointer;
+
+ input[type="radio"] {
+ margin: 0;
+ cursor: pointer;
+ }
+ }
+ }
+}
+
+.publications-box {
+ margin: 12px 0;
+ padding: 6px 8px;
+ border: 1px solid var(--grey-2);
+ border-radius: 4px;
+
+ .box-title {
+ font-weight: 500;
+ margin-bottom: 6px;
+ padding-bottom: 3px;
+ border-bottom: 1px solid var(--grey-2);
+ }
+
+ button {
+ margin-bottom: 4px;
+ }
+
+ .bibtex-mode {
+ display: flex;
+ flex-direction: column;
+ gap: 3px;
+ margin-top: 2px;
+ font-size: 0.9em;
+
+ label {
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ cursor: pointer;
+
+ input[type="radio"] {
+ margin: 0;
+ cursor: pointer;
+ }
+ }
+ }
+}
diff --git a/src/themes/cuiv/index.ejs b/src/themes/cuiv/index.ejs
new file mode 100644
index 0000000..f3bdaac
--- /dev/null
+++ b/src/themes/cuiv/index.ejs
@@ -0,0 +1,804 @@
+<%
+function linkInDiv(url) {
+ if (!url) return ''
+ return `
`
+}
+
+// Format citation from structured data
+function formatCitation(pub, index) {
+ // If we have pre-formatted HTML (from BibTeX), use it but replace the number
+ if (pub.formattedHTML) {
+ // Replace any number in [x] with current index + 1
+ return pub.formattedHTML.replace(/\[\d+\]/, `[${index + 1}]`);
+ }
+
+ // Otherwise format from structured data
+ const parts = [];
+ if (pub.authors && pub.authors.length > 0) {
+ parts.push(pub.authors.join(', '));
+ }
+ if (pub.name) parts.push(`"${pub.name}"`);
+ if (pub.publisher) parts.push(pub.publisher);
+ if (pub.releaseDate) parts.push(pub.releaseDate);
+
+ // Return formatted HTML with the same structure as Citation.js output
+ return `
[${index + 1}]
${parts.join(', ')}
`;
+}
+
+function formatLocation(loc) {
+ const cityToCountry = ['city', 'region', 'postalCode', 'countryCode'].map(key => loc[key]).filter(v => v).join(', ')
+ if (!loc.address) return cityToCountry
+ return `${loc.address}, ${cityToCountry}`
+}
+
+function hasItems(arr) {
+ return Array.isArray(arr) && arr.length > 0
+}
+
+function dateRange(item, level) {
+ let {startDate, endDate} = item
+ if (!startDate && !endDate) return ''
+ let format
+ // level: 1: year, 2: month, 3: day
+ switch (level) {
+ case 1:
+ format = 'YYYY'
+ break;
+ case 2:
+ format = 'MMM YYYY'
+ break;
+ }
+ if (format) {
+ startDate = startDate ? fn.reformatDate(startDate, format) : ''
+ endDate = endDate ? fn.reformatDate(endDate, format) : ''
+ }
+ let result = ''
+ if (startDate && endDate) {
+ result = `${startDate} – ${endDate}`
+ } else if (startDate) {
+ result = `${startDate} – Present`
+ } else if (endDate) {
+ result = `Until ${endDate}`
+ }
+ return `
${result}
`
+}
+
+function date(item, level) {
+ let {date} = item
+ if (!date) return ''
+ let format
+ // level: 1: year, 2: month, 3: day
+ switch (level) {
+ case 1:
+ format = 'YYYY'
+ break;
+ case 2:
+ format = 'MMM YYYY'
+ break;
+ }
+ if (format) {
+ date = date ? fn.reformatDate(date, format) : ''
+ }
+ return `
${date}
`
+}
+%>
+
+
+ ☀️
+ 🌙
+
+
+
+
+ <%= cv.basics.name %>
+ <% if (cv.basics.label) { %>
+ <%= cv.basics.label %>
+ <% } %>
+
+
+ <% if (cv.basics.summary) { %>
+ <%- fn.renderMarkdown(cv.basics.summary) %>
+ <% } %>
+
+
+
+ <% if (hasItems(cv.education)) { %>
+
+
+ <% for (const item of cv.education) { %>
+
+
+
<%= item.institution %>
+
+
+
+ <%= item.studyType %> <%= item.area %>
+
+
+ <%
+ const startYear = item.startDate ? item.startDate.split('-')[0] : '';
+ const endYear = item.endDate === 'Present' ? 'Present' :
+ item.endDate ? item.endDate.split('-')[0] : '';
+ %>
+ <%= startYear %><% if (startYear && endYear) { %> - <% } %><%= endYear %>
+
+
+ <% if (item.score) { %>
Overall GPA: <%= item.score %>
<% } %>
+ <% if (item.dissertation) { %>
+
Dissertation: <%= item.dissertation %>
+ <% } %>
+ <% if (item.thesis) { %>
+
Thesis: <%= item.thesis %>
+ <% } %>
+ <% if (item.courses && item.courses.length > 0) { %>
+
+ Courses: <%= item.courses.join('; ') %>
+
+ <% } %>
+
+ <% } %>
+
+ <% } %>
+
+
+ <% if (hasItems(cv.work)) { %>
+
+
+ <% for (const item of cv.work) { %>
+
+ <% if (item.name !== (cv.work[cv.work.indexOf(item) - 1] || {}).name) { %>
+
+
<%= item.name %>
+
+ <% } %>
+
+
<%= item.position %>
+
+ <%
+ const startYear = item.startDate ? item.startDate.split('-')[0] : '';
+ const endYear = item.endDate === 'Present' ? 'Present' :
+ item.endDate ? item.endDate.split('-')[0] : 'Present';
+ %>
+ <%= startYear %><% if (startYear && endYear) { %> - <% } %><%= endYear %>
+
+
+
<%- fn.renderMarkdown(item.summary) %>
+ <% if (item.highlights && item.highlights.length > 0) { %>
+
+
+ <% for (const hl of item.highlights) { %>
+ <%- fn.renderMarkdown(hl, true) %>
+ <% } %>
+
+
+ <% } %>
+
+ <% } %>
+
+ <% } %>
+
+
+ <% if (hasItems(cv.researchAreas)) { %>
+
+
+ <% for (const item of cv.researchAreas) { %>
+
+
<%= item.name %>
+ <% if (item.description) { %>
+
<%- fn.renderMarkdown(item.description) %>
+ <% } %>
+ <% if (item.keywords && Array.isArray(item.keywords) && item.keywords.length > 0) { %>
+
+ <% for (const kw of item.keywords) { %>
+ <%= kw %>
+ <% } %>
+
+ <% } %>
+
+ <% } %>
+
+ <% } %>
+
+
+ <%
+ // Group publications by type, defaulting to 'other' if type not specified
+ const journalPubs = cv.publications?.filter(p => p.type === 'journal') || []
+ const conferencePubs = cv.publications?.filter(p => p.type === 'conference') || []
+ const otherPubs = cv.publications?.filter(p => !p.type || p.type === 'other') || []
+
+ // Only show section if there are publications
+ if (hasItems(journalPubs) || hasItems(conferencePubs) || hasItems(otherPubs)) {
+ %>
+
+
+
+ <% if (hasItems(journalPubs)) { %>
+
+
Journal Papers
+ <% for (let i = 0; i < journalPubs.length; i++) { %>
+
+
<%- formatCitation(journalPubs[i], i) %>
+ <%- linkInDiv(journalPubs[i].url) %>
+
+ <% } %>
+
+ <% } %>
+
+ <% if (hasItems(conferencePubs)) { %>
+
+
Conference Papers
+ <% for (let i = 0; i < conferencePubs.length; i++) { %>
+
+
<%- formatCitation(conferencePubs[i], i) %>
+ <%- linkInDiv(conferencePubs[i].url) %>
+
+ <% } %>
+
+ <% } %>
+
+ <% if (hasItems(otherPubs)) { %>
+
+
Other Publications
+ <% for (let i = 0; i < otherPubs.length; i++) { %>
+
+
<%- formatCitation(otherPubs[i], i) %>
+ <%- linkInDiv(otherPubs[i].url) %>
+
+ <% } %>
+
+ <% } %>
+
+ <% } %>
+
+
+ <% if (hasItems(cv.projects)) { %>
+
+
+ <% for (const item of cv.projects) { %>
+
+
+
<%= item.name %>
+ <%- linkInDiv(item.url) %>
+
+
+ <% if (item.roles && item.roles.length > 0) { %>
+
<%= item.roles.join(', ') %>
+ <% } %>
+ <%- dateRange(item, 2) %>
+
+
<%- fn.renderMarkdown(item.description, true) %>
+ <% if (item.highlights && item.highlights.length > 0) { %>
+ <% if (item.highlights.length === 1) { %>
+
<%- fn.renderMarkdown(item.highlights[0], true) %>
+ <% } else { %>
+
+
+ <% for (const hl of item.highlights) { %>
+ <%- fn.renderMarkdown(hl, true) %>
+ <% } %>
+
+
+ <% } %>
+ <% } %>
+ <% if (item.keywords && Array.isArray(item.keywords) && item.keywords.length > 0) { %>
+
+ <% for (const kw of item.keywords) { %>
+ <%= kw %>
+ <% } %>
+
+ <% } %>
+
+ <% } %>
+
+ <% } %>
+
+
+ <% if (hasItems(cv.teaching)) { %>
+
+
+
Teaching Experience
+
+
+ <% for (const item of cv.teaching) { %>
+
+
+
+
<%= item.courseCode %>
+
<%= item.courseName %>
+
+
<%= item.terms.join(', ') %>
+
+ <% if (item.description) { %>
+
<%- fn.renderMarkdown(item.description) %>
+ <% } %>
+
+ <% } %>
+
+ <% } %>
+
+
+ <% if (cv.mentoring) { %>
+
+
+
+ <% if (hasItems(cv.mentoring.current_students)) { %>
+ Current Students
+
+
+ <% const phd_students = cv.mentoring.current_students.filter(s => s.level === 'Ph.D.') %>
+ <% if (phd_students.length > 0) { %>
+ Ph.D. Students
+ <% for (const student of phd_students) { %>
+
+
+
+ <% if (student.startDate) { %>
+
Since <%= student.startDate %>
+ <% } %>
+
+ <% if (student.dissertation) { %>
+
+
Dissertation: <%= student.dissertation %>
+
+ <% } %>
+
+ <% } %>
+ <% } %>
+
+
+ <% const ms_students = cv.mentoring.current_students.filter(s => s.level === 'M.S.') %>
+ <% if (ms_students.length > 0) { %>
+ Master's Students
+ <% for (const student of ms_students) { %>
+
+
+
+ <% if (student.startDate) { %>
+
Since <%= student.startDate %>
+ <% } %>
+
+
+ <% if (student.type) { %>
+
<%= student.type %> Program
+ <% } %>
+ <% if (student.type === 'Thesis' && student.thesis_title) { %>
+
Thesis: <%= student.thesis_title %>
+ <% } %>
+
+
+ <% } %>
+ <% } %>
+
+
+ <% const ug_students = cv.mentoring.current_students.filter(s => s.level === 'Undergraduate') %>
+ <% if (ug_students.length > 0) { %>
+ Undergraduate Researchers
+ <% for (const student of ug_students) { %>
+
+
+
+ <% if (student.period) { %>
+
<%= student.period %>
+ <% } %>
+
+ <% if (student.program) { %>
+
+
<%= student.program %>
+
+ <% } %>
+
+ <% } %>
+ <% } %>
+ <% } %>
+
+ <% if (hasItems(cv.mentoring.committee_service)) { %>
+ Ph.D. Committee Service
+ <% const current = cv.mentoring.committee_service.filter(s => s.status === 'Current') %>
+ <% const completed = cv.mentoring.committee_service.filter(s => s.status === 'Completed') %>
+
+ <% if (current.length > 0) { %>
+ Current
+ <% for (const student of current) { %>
+
+
+
<%= student.name %>
+
<%= student.institution %>
+
+
+
Dissertation: <%= student.dissertation %>
+
+
+ <% } %>
+ <% } %>
+
+ <% if (completed.length > 0) { %>
+ Completed
+ <% for (const student of completed) { %>
+
+
+
<%= student.name %>
+
<%= student.institution %>
+
+
+
Dissertation: <%= student.dissertation %>
+ <% if (student.completionDate) { %>
+
Completed: <%= student.completionDate %>
+ <% } %>
+
+
+ <% } %>
+ <% } %>
+ <% } %>
+
+ <% if (hasItems(cv.mentoring.past_students)) { %>
+ Past Students
+ <% for (const student of cv.mentoring.past_students) { %>
+
+
+
+
<%= student.level %>
+
+
+
<%= student.institution %>
+ <% if (student.startDate || student.endDate) { %>
+
+ <%= student.startDate || '' %><% if (student.startDate && student.endDate) { %> - <% } %><%= student.endDate || '' %>
+
+ <% } %>
+ <% if (student.level === 'Ph.D.' && student.dissertation) { %>
+
Dissertation: <%= student.dissertation %>
+ <% } %>
+ <% if (student.level === 'M.S.') { %>
+ <% if (student.type) { %>
+
<%= student.type %> Program
+ <% } %>
+ <% if (student.type === 'Thesis' && student.thesis_title) { %>
+
Thesis: <%= student.thesis_title %>
+ <% } %>
+ <% } %>
+ <% if (student.level === 'Undergraduate' && student.program) { %>
+
<%= student.program %>
+ <% } %>
+ <% if (student.placement) { %>
+
+ <% if (student.placement.position && student.placement.organization) { %>
+ Currently <%= student.placement.position %> at <%= student.placement.organization %>
+ <% if (student.placement.location) { %>
+ , <%= student.placement.location %>
+ <% } %>
+ <% } else { %>
+ <% if (student.placement.position) { %>
+ Currently <%= student.placement.position %>
+ <% } %>
+ <% if (student.placement.organization) { %>
+ at <%= student.placement.organization %>
+ <% } %>
+ <% if (student.placement.location) { %>
+ , <%= student.placement.location %>
+ <% } %>
+ <% } %>
+
+ <% } %>
+
+
+ <% } %>
+ <% } %>
+
+ <% } %>
+
+
+ <% if (cv.presentations) { %>
+
+
+
+ <% if (hasItems(cv.presentations.invitedTalks)) { %>
+ Invited Talks
+ <% for (const item of cv.presentations.invitedTalks) { %>
+
+
+
<%= item.title || 'TBD' %>
+ <%- date(item, 2) %>
+
+
+ <%= item.venue %> (<%= item.type %>)
+
+ <% if (item.description) { %>
+
<%- fn.renderMarkdown(item.description) %>
+ <% } %>
+
+ <% } %>
+ <% } %>
+
+ <% if (hasItems(cv.presentations.conferences)) { %>
+ Conference Presentations
+ <% for (const item of cv.presentations.conferences) { %>
+
+
+
<%= item.title %>
+ <%- date(item, 2) %>
+
+
+ <%= item.venue %> (<%= item.type %>)
+ <% if (item.location) { %> - <%= item.location %><% } %>
+
+ <% if (item.description) { %>
+
<%- fn.renderMarkdown(item.description) %>
+ <% } %>
+ <%- linkInDiv(item.url) %>
+
+ <% } %>
+ <% } %>
+
+ <% if (hasItems(cv.presentations.outreach)) { %>
+ Outreach Activities
+ <% for (const item of cv.presentations.outreach) { %>
+
+
+
<%= item.activity %>
+ <%- date(item, 2) %>
+
+ <% if (item.description) { %>
+
<%- fn.renderMarkdown(item.description) %>
+ <% } %>
+ <%- linkInDiv(item.url) %>
+
+ <% } %>
+ <% } %>
+
+ <% } %>
+
+
+ <% if (hasItems(cv.awards)) { %>
+
+
+ <% for (const item of cv.awards) { %>
+
+
+
<%= item.title %>
+ <%- date(item, 2) %>
+
+
<%= item.awarder %>
+ <% if (item.summary) { %>
+
<%- fn.renderMarkdown(item.summary) %>
+ <% } %>
+
+ <% } %>
+
+ <% } %>
+
+
+ <% if (cv.services) { %>
+
+
+
Professional Services
+
+
+
+
+ <% if (hasItems(cv.services.editorialBoards)) { %>
+ Editorial Service
+ <% for (const item of cv.services.editorialBoards) { %>
+
+
+
<%= item.role %>
+
<%= item.period %>
+
+
<%= item.journal %>
+ <% if (item.details) { %>
+
<%- fn.renderMarkdown(item.details) %>
+ <% } %>
+
+ <% } %>
+ <% } %>
+
+
+ <% if (hasItems(cv.services.societyServices)) { %>
+ Society Service
+ <% for (const item of cv.services.societyServices) { %>
+
+
+
<%= item.role %>
+
<%= item.period %>
+
+
<%= item.organization %>
+
+ <% } %>
+ <% } %>
+
+
+ <% if (hasItems(cv.services.reviewServices)) { %>
+ Review Service
+ <% for (const item of cv.services.reviewServices) { %>
+
+
+
<%= item.type %>
+
<%= item.year %>
+
+
<%= item.organization %>
+ <% if (item.details) { %>
+
<%- fn.renderMarkdown(item.details) %>
+ <% } %>
+
+ <% } %>
+ <% } %>
+
+
+ <% if (hasItems(cv.services.departmental)) { %>
+ Department Service
+ <% for (const item of cv.services.departmental) { %>
+
+
+
<%= item.role %>
+
<%= item.period %>
+
+
<%= item.committee %>
+
+ <% } %>
+ <% } %>
+
+ <% } %>
+
+
+ <% if (hasItems(cv.certificates)) { %>
+
+
+ <% for (const item of cv.certificates) { %>
+
+
+
<%= item.name %>
+ <%- date(item, 2) %>
+
+
<%= item.issuer %>
+ <%- linkInDiv(item.url) %>
+
+ <% } %>
+
+ <% } %>
+
+
+ <% if (hasItems(cv.skills)) { %>
+
+
+
+ <% for (const item of cv.skills) { %>
+
+
<%= item.name %>
+
<%= item.level %>
+ <% if (item.summary) { %>
+
<%- fn.renderMarkdown(item.summary) %>
+ <% } %>
+ <% if (item.keywords && Array.isArray(item.keywords) && item.keywords.length > 0) { %>
+
+ <% for (const kw of item.keywords) { %>
+ <%= kw %>
+ <% } %>
+
+ <% } %>
+
+ <% } %>
+
+
+ <% } %>
+
+
+ <% if (hasItems(cv.languages)) { %>
+
+
+
+ <% for (const item of cv.languages) { %>
+
+
<%= item.language %>
+
<%= item.fluency %>
+ <% if (item.summary) { %>
+
<%- fn.renderMarkdown(item.summary) %>
+ <% } %>
+
+ <% } %>
+
+
+ <% } %>
+
+
+ <% if (cv.meta) { %>
+
+ <% if (cv.meta.version) { %>
+ Version: <%= cv.meta.version %>
+ <% } %>
+ <% if (cv.meta.lastModified) { %>
+ Last modified: <%= cv.meta.lastModified.slice(0, 10) %>
+ <% } %>
+
+ <% } %>
\ No newline at end of file
diff --git a/src/themes/cuiv/index.js b/src/themes/cuiv/index.js
new file mode 100644
index 0000000..82f8416
--- /dev/null
+++ b/src/themes/cuiv/index.js
@@ -0,0 +1,81 @@
+import ejs from 'ejs';
+import { upsertStyleTag } from '../../lib/utils';
+import { getRenderData, varNamePrimaryColor } from '../data';
+import template from './index.ejs';
+import style from './index.scss?inline';
+
+export const defaultColor = '#cc0000' // matches :root { --color-primary } in index.scss
+
+// Get system color scheme
+function getSystemColorMode() {
+ const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
+ console.log('System color mode:', isDark ? 'dark' : 'light');
+ return isDark ? 'dark' : 'light';
+}
+
+// Make toggleTheme available globally
+window.toggleTheme = function() {
+ const cvContainer = document.querySelector('.cv-container');
+ const themeToggle = document.querySelector('.theme-toggle');
+ if (!cvContainer || !themeToggle) {
+ console.log('Toggle failed: Elements not found', { cvContainer, themeToggle });
+ return;
+ }
+
+ const currentMode = cvContainer.getAttribute('data-color-mode') || getSystemColorMode();
+ console.log('Current color mode before toggle:', currentMode);
+ console.log('Current attributes:', {
+ container: cvContainer.getAttribute('data-color-mode'),
+ toggle: themeToggle.getAttribute('data-mode')
+ });
+
+ const newMode = currentMode === 'light' ? 'dark' : 'light';
+ console.log('New color mode:', newMode);
+
+ cvContainer.setAttribute('data-color-mode', newMode);
+ themeToggle.setAttribute('data-mode', newMode);
+ localStorage.setItem('colorMode', newMode);
+
+ console.log('After toggle attributes:', {
+ container: cvContainer.getAttribute('data-color-mode'),
+ toggle: themeToggle.getAttribute('data-mode'),
+ stored: localStorage.getItem('colorMode')
+ });
+}
+
+export function renderTheme(el, data, primaryColor) {
+ console.log('Rendering theme...');
+ el.innerHTML = ejs.render(template, getRenderData(data));
+ upsertStyleTag('cv-style', style);
+ document.documentElement.style.setProperty(varNamePrimaryColor, primaryColor);
+
+ // Initialize color mode based on stored preference or system preference
+ const storedMode = localStorage.getItem('colorMode');
+ console.log('Stored color mode:', storedMode);
+ const initialMode = storedMode || getSystemColorMode();
+ console.log('Initial color mode:', initialMode);
+
+ const cvContainer = document.querySelector('.cv-container');
+ const themeToggle = document.querySelector('.theme-toggle');
+ if (cvContainer && themeToggle) {
+ console.log('Setting initial color mode...');
+ cvContainer.setAttribute('data-color-mode', initialMode);
+ themeToggle.setAttribute('data-mode', initialMode);
+ console.log('Initial attributes set:', {
+ container: cvContainer.getAttribute('data-color-mode'),
+ toggle: themeToggle.getAttribute('data-mode')
+ });
+ } else {
+ console.log('Elements not found during initialization:', { cvContainer, themeToggle });
+ }
+
+ // Listen for system theme changes
+ window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
+ if (!localStorage.getItem('colorMode')) {
+ const newMode = e.matches ? 'dark' : 'light';
+ console.log('System color mode changed:', newMode);
+ cvContainer.setAttribute('data-color-mode', newMode);
+ themeToggle.setAttribute('data-mode', newMode);
+ }
+ });
+}
\ No newline at end of file
diff --git a/src/themes/cuiv/index.scss b/src/themes/cuiv/index.scss
new file mode 100644
index 0000000..5c71557
--- /dev/null
+++ b/src/themes/cuiv/index.scss
@@ -0,0 +1,721 @@
+/* Naming convention: https://ricostacruz.com/rscss/ */
+
+// Colors
+$color-black: #000;
+$color-text-dim: #777;
+$color-text-dimmer: #999;
+$color-border-dim: #aaa;
+$color-border-dimmer: #ccc;
+
+// Font sizes
+$fz-1: 32px;
+$fz-2: 20px;
+$fz-3: 16px;
+$fz-4: 14px;
+$fz-5: 13px;
+$fz-6: 12px;
+$lh-p: 1.3;
+
+// Layout system
+:root {
+ // Theme colors
+ --bg-color: #fff;
+ --text-color: #{$color-black};
+ --text-dim: #{$color-text-dim};
+ --text-dimmer: #{$color-text-dimmer};
+ --border-dim: #{$color-border-dim};
+ --border-dimmer: #{$color-border-dimmer};
+ --keyword-bg: #f1f1f1;
+
+ // Default color that can be overridden by meta.colorPrimary
+ --color-primary: #aaa;
+
+ // Time display properties
+ --time-color: var(--text-color);
+ --time-font-style: italic;
+ --time-min-width: 7em;
+ --time-text-align: right;
+ --time-content-ratio: 75%;
+
+ // Spacing system
+ --content-indent: 10em; // Left indentation for content
+ --content-padding: 1em; // Right padding for content
+ --section-gap-large: 1.2em; // Large gaps between sections
+ --section-gap-medium: 1em; // Medium gaps for major subsections
+ --section-gap: 0.8em; // Normal gaps between sections
+ --section-gap-small: 0.4em; // Small gaps between items
+ --section-gap-tiny: 0.2em; // Tiny gaps for tight spacing
+
+ // Print settings
+ --print-margin-top: 0.5in; // Standard Letter margins
+ --print-margin-bottom: 0.5in;
+ --print-margin-left: 0.5in;
+ --print-margin-right: 0.5in;
+}
+
+// Dark mode
+:root[data-theme="dark"] {
+ --bg-color: #1a1a1a;
+}
+
+body {
+ margin: 0;
+ padding: 1rem;
+ min-height: 100vh;
+}
+
+.theme-toggle {
+ position: absolute;
+ top: 1rem;
+ right: 1rem;
+ background: #fff;
+ border: 1px solid var(--border-dimmer);
+ border-radius: 20px;
+ padding: 0.5rem;
+ cursor: pointer;
+ z-index: 1000;
+ box-shadow: 0 2px 5px rgba(0,0,0,0.1);
+
+ @media print {
+ display: none;
+ }
+
+ .theme-icon {
+ display: none;
+ font-size: 1.2rem;
+ line-height: 1;
+
+ &.light {
+ display: inline; // Show sun icon by default
+ }
+ }
+
+ // Override default when dark mode is explicitly set
+ &[data-mode="dark"] {
+ .theme-icon.light { display: none; }
+ .theme-icon.dark { display: inline; }
+ }
+}
+
+.cv-container {
+ background: #fff;
+ color: var(--text-color);
+ font-size: 14px;
+ font-family: system-ui, sans-serif;
+ max-width: 8.5in; // Letter width
+ margin: 0 auto; // Center on screen
+ padding: 2rem;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+ transition: background-color 0.3s, color 0.3s;
+
+ &[data-color-mode="dark"] {
+ background: #1a1a1a;
+ color: #fff;
+ --text-color: #fff;
+ --text-dim: #aaa;
+ --text-dimmer: #888;
+ --border-dim: #666;
+ --border-dimmer: #444;
+ --keyword-bg: #2a2a2a;
+ }
+
+ @media print {
+ width: 100%;
+ max-width: none;
+ margin: 0;
+ padding: 0;
+ box-shadow: none;
+ background: #fff !important;
+ color: #{$color-black} !important;
+ --text-color: #{$color-black} !important;
+ --text-dim: #{$color-text-dim} !important;
+ --text-dimmer: #{$color-text-dimmer} !important;
+ --border-dim: #{$color-border-dim} !important;
+ --border-dimmer: #{$color-border-dimmer} !important;
+
+ // Only protect header and footer from breaking
+ .basics-section {
+ break-inside: avoid;
+ page-break-inside: avoid;
+ }
+ }
+}
+
+.date-range {
+ font-style: italic;
+}
+
+a {
+ color: var(--text-dim);
+ text-decoration: none;
+}
+
+a:hover {
+ text-decoration: underline;
+}
+
+section {
+ .section-title {
+ margin-top: var(--section-gap-large);
+ margin-bottom: var(--section-gap);
+ display: flex;
+
+ h2 {
+ font-size: $fz-2;
+ font-weight: 600;
+ color: var(--color-primary);
+ margin: 0;
+ }
+ .line {
+ flex-grow: 1;
+ margin: 12px 0 0 1em;
+ height: 2px;
+ background-color: var(--color-primary);
+ }
+ }
+
+ .section-item {
+ margin-top: var(--section-gap-small);
+ margin-left: var(--content-indent);
+ padding-right: var(--content-padding);
+
+ h3 {
+ font-size: $fz-3;
+ line-height: 1.2;
+ margin: 0;
+ }
+
+ h4 {
+ font-size: $fz-4;
+ line-height: 1.2;
+ margin: 0;
+ }
+
+ ul {
+ margin: .3em 0;
+ padding-left: 1.5em;
+ li {
+ margin-bottom: .15em;
+ }
+ }
+ }
+
+ svg {
+ vertical-align: bottom;
+ position: relative;
+ bottom: 1px;
+ }
+ .row {
+ margin: .15em 0;
+ }
+ .p, p {
+ line-height: $lh-p;
+ margin: .3em 0;
+ }
+ .subtitle {
+ font-size: $fz-4;
+ // font-weight: 300;
+
+ @media screen and (max-width: 500px) {
+ display: flex;
+ align-items: center;
+ }
+ }
+
+ .space-between {
+ display: flex;
+ justify-content: space-between;
+
+ a {
+ color: var(--text-dim);
+ }
+ }
+
+ .keywords {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 6px 8px;
+ margin-top: .8em;
+
+ .item {
+ font-size: $fz-5;
+ color: var(--text-color);
+ padding: 2px 8px;
+ background: var(--keyword-bg);
+ border-radius: 4px;
+ }
+ }
+
+ .two-columns {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 0 1.5em;
+
+ @media screen and (max-width: 500px) {
+ grid-template-columns: 1fr;
+ }
+ }
+
+ /* helpers */
+ ._fz-4 {
+ font-size: $fz-4;
+ }
+ ._ul {
+ text-decoration: underline;
+ }
+ ._text-dim {
+ color: var(--text-dim);
+ }
+}
+
+@media print {
+ section {
+ .section-title {
+ margin-top: var(--section-gap);
+ margin-bottom: var(--section-gap-small);
+ }
+ .section-item {
+ margin-top: var(--section-gap-small);
+ margin-left: var(--content-indent);
+ padding-right: var(--content-padding);
+ }
+ }
+}
+
+.basics-section {
+ h1 {
+ font-size: $fz-1;
+ font-weight: 600;
+ margin: .1em 0;
+ }
+ .subtitle {
+ font-size: $fz-2;
+ }
+ .contact {
+ color: var(--text-dim);
+ display: flex;
+ flex-wrap: wrap;
+ gap: .4em 1em;
+
+ @media screen and (max-width: 500px) {
+ flex-direction: column;
+ }
+
+ .item {
+ a {
+ color: var(--color-primary);
+ &.colored-link {
+ color: var(--color-primary);
+ text-decoration: none;
+ &:hover {
+ text-decoration: underline;
+ }
+ }
+ }
+ }
+ }
+ .colored-links {
+ color: var(--color-primary);
+ display: flex;
+ flex-wrap: nowrap;
+ gap: 1em;
+
+ .item {
+ white-space: nowrap;
+ }
+
+ a {
+ color: inherit;
+ text-decoration: none;
+ &:hover {
+ text-decoration: underline;
+ }
+ }
+ }
+}
+
+// Shared styles for sections with smaller text
+%academic-section {
+ font-size: $fz-5;
+
+ .section-item {
+ line-height: 1.4;
+
+ h3 {
+ font-size: $fz-4;
+ }
+
+ h4 {
+ font-size: $fz-5;
+ }
+
+ .subtitle {
+ font-size: $fz-5;
+ }
+ }
+}
+
+// Shared time display style and layout
+%time-display {
+ color: var(--text-color);
+ font-style: var(--time-font-style);
+ min-width: var(--time-min-width);
+ text-align: var(--time-text-align);
+}
+
+%time-container {
+ display: flex;
+ justify-content: space-between;
+
+ // Content side
+ > *:first-child {
+ max-width: var(--time-content-ratio);
+ }
+
+ // Time side - include all possible time-related classes
+ > .year,
+ > .date-range,
+ > .period,
+ > .terms {
+ @extend %time-display;
+ }
+}
+
+.education-section {
+ @extend %academic-section;
+ .space-between {
+ @extend %time-container;
+ }
+}
+
+.work-section {
+ @extend %academic-section;
+ .space-between {
+ @extend %time-container;
+ }
+}
+
+.projects-section {
+ @extend %academic-section;
+ .space-between {
+ @extend %time-container;
+ }
+}
+
+.sideprojects-section {
+ .description p:last-of-type {
+ margin-bottom: 0;
+ }
+}
+
+.research-metrics-section {
+ .metrics {
+ display: flex;
+ gap: 2em;
+ font-size: $fz-4;
+
+ .item {
+ color: var(--text-dim);
+ }
+ }
+}
+
+.academic-appointments-section {
+ .appointment {
+ .department {
+ color: var(--text-dim);
+ }
+ .institution {
+ font-style: italic;
+ }
+ }
+}
+
+.teaching-section {
+ .course {
+ .row {
+ @extend %time-container;
+ }
+ .course-info {
+ display: flex;
+ gap: 1em;
+ }
+ .code {
+ font-weight: 600;
+ }
+ .terms {
+ @extend %time-display;
+ }
+ }
+}
+
+.grants-section {
+ .grant {
+ .amount {
+ font-weight: 600;
+ color: var(--color-primary);
+ }
+ .agency {
+ font-style: italic;
+ color: var(--text-dim);
+ }
+ }
+}
+
+.mentoring-section {
+ @extend %academic-section;
+
+ h3 {
+ margin-top: var(--section-gap-medium);
+ margin-bottom: var(--section-gap-small);
+ color: var(--color-primary);
+ font-size: $fz-4;
+ }
+
+ h4 {
+ margin-top: 0.3em;
+ margin-bottom: 0.2em;
+ color: var(--text-dim);
+ font-size: $fz-5;
+ }
+
+ .student {
+ margin-top: 0.2em;
+
+ .row {
+ @extend %time-container;
+ }
+
+ .name {
+ font-weight: 600;
+ }
+
+ .details {
+ margin-top: 0.1em;
+ }
+
+ .program, .period {
+ color: var(--text-dim);
+ }
+
+ .dissertation, .thesis {
+ margin-top: 0.1em;
+ }
+ }
+}
+
+.editorial-positions-section {
+ .position {
+ .role {
+ font-weight: 600;
+ }
+ .journal {
+ font-style: italic;
+ color: var(--text-dim);
+ }
+ }
+}
+
+.professional-services-section {
+ @extend %academic-section;
+
+ h3 {
+ margin-top: var(--section-gap-medium);
+ margin-bottom: var(--section-gap-small);
+ color: var(--color-primary);
+ font-size: $fz-4;
+ }
+
+ // Editorial Boards
+ .service {
+ margin-top: var(--section-gap-tiny);
+
+ .space-between {
+ @extend %time-container;
+ }
+
+ .role {
+ font-weight: 600;
+ }
+
+ .journal, .organization, .committee {
+ font-style: italic;
+ color: var(--text-dim);
+ }
+
+ .details {
+ margin: var(--section-gap-tiny) 0;
+ font-size: $fz-5;
+ }
+ }
+}
+
+.presentations-section {
+ @extend %academic-section;
+
+ h3 {
+ margin-top: var(--section-gap-medium);
+ margin-bottom: var(--section-gap-small);
+ color: var(--color-primary);
+ font-size: $fz-4;
+ }
+
+ .talk, .presentation, .outreach {
+ margin-top: var(--section-gap-tiny);
+
+ .space-between {
+ @extend %time-container;
+ }
+
+ .venue {
+ margin-top: var(--section-gap-tiny);
+ font-size: $fz-5;
+ }
+
+ .description {
+ margin: var(--section-gap-tiny) 0;
+ font-size: $fz-5;
+ }
+ }
+}
+
+.awards-section {
+ @extend %academic-section;
+ .space-between {
+ @extend %time-container;
+ }
+}
+
+.certificates-section {
+ @extend %academic-section;
+ .space-between {
+ @extend %time-container;
+ }
+}
+
+.publications-section {
+ @extend %academic-section;
+
+ .subsection {
+ margin-bottom: var(--section-gap-medium);
+
+ h3 {
+ color: var(--color-primary);
+ margin-bottom: var(--section-gap-small);
+ font-size: $fz-3;
+ }
+ }
+
+ .publication {
+ margin-bottom: var(--section-gap-small);
+
+ .citation {
+ line-height: $lh-p;
+
+ strong { // For CV owner highlighting
+ color: var(--color-primary);
+ }
+
+ em { // For mentee highlighting
+ font-style: italic;
+ color: var(--text-dim);
+ }
+
+ // Make citation number and content appear inline
+ .csl-bib-body {
+ display: flex;
+ align-items: flex-start;
+ }
+
+ .csl-entry {
+ display: flex;
+ align-items: flex-start;
+ flex: 1;
+
+ .csl-left-margin {
+ min-width: 2.5em;
+ padding-right: 0.5em;
+ }
+
+ .csl-right-inline {
+ flex: 1;
+ }
+ }
+ }
+
+ .url {
+ margin-top: var(--section-gap-tiny);
+ font-size: $fz-5;
+ margin-left: 3em; // Match citation content alignment (2.5em + 0.5em padding)
+ }
+
+ .summary {
+ margin-top: var(--section-gap-tiny);
+ color: var(--text-dim);
+ font-size: $fz-5;
+ }
+ }
+
+ .url a {
+ color: var(--text-dim);
+ &:hover {
+ color: var(--text-dim);
+ }
+ }
+}
+
+.research-areas-section {
+ @extend %academic-section;
+
+ .section-item {
+ h3 {
+ font-size: $fz-4;
+ margin-bottom: 0.2em;
+ }
+
+ .description {
+ font-size: $fz-5;
+ line-height: 1.3;
+ }
+
+ .keywords {
+ margin-top: 0.4em;
+ }
+ }
+}
+
+// Update all other hardcoded color references
+.research-metrics-section .metrics .item,
+.academic-appointments-section .department,
+.teaching-section .terms,
+.grants-section .agency,
+.mentoring-section h4,
+.mentoring-section .program,
+.mentoring-section .period,
+.editorial-positions-section .journal,
+.professional-services-section .journal,
+.professional-services-section .organization,
+.professional-services-section .committee,
+.publications-section .citation em,
+.publications-section .summary,
+.publications-section .url a {
+ color: var(--text-dim);
+}
+
+footer {
+ margin-top: var(--section-gap-large);
+ padding-top: var(--section-gap);
+ border-top: 1px solid var(--border-dimmer);
+ color: var(--text-dimmer);
+ font-size: $fz-5;
+
+ display: flex;
+ gap: 1em;
+}
+
+// Dark mode override
+.cv-container[data-color-mode="dark"] {
+ .keywords .item {
+ background: #2a2a2a;
+ }
+}
diff --git a/src/themes/index.js b/src/themes/index.js
index 3bb80c8..d927617 100644
--- a/src/themes/index.js
+++ b/src/themes/index.js
@@ -6,45 +6,32 @@ import {
varNamePrimaryColor,
} from './data';
-const themes = {}
+import { renderTheme as renderReorxTheme, defaultColor as reorxColor } from './reorx/index.js'
+import { renderTheme as renderCuivTheme, defaultColor as cuivColor } from './cuiv/index.js'
-const themeNames = ['reorx']
-
-// https://vitejs.dev/guide/features.html#disabling-css-injection-into-the-page
-// note that `?raw` (https://vitejs.dev/guide/assets.html#importing-asset-as-string)
-// cannot be used because we need vite to transform scss into css
-const styleMoudules = import.meta.glob("./*/index.scss", { "query": "?inline" })
-
-for (const name of themeNames) {
- const templateModule = await import(`./${name}/index.ejs`)
-
- // https://vitejs.dev/guide/features.html#glob-import
- const styleModule = await styleMoudules[`./${name}/index.scss`]()
-
- themes[name] = {
- template: templateModule.default,
- style: styleModule.default,
- }
+const themes = {
+ reorx: renderReorxTheme,
+ cuiv: renderCuivTheme,
}
-// set default
-themes.default = themes.reorx
-
-export function getTheme(name) {
- return themes[name]
+const themeColors = {
+ reorx: reorxColor,
+ cuiv: cuivColor,
}
-export function renderTheme(template, cvData, options) {
- return ejs.render(template, getRenderData(cvData), options)
+export function getAvailableThemes() {
+ return Object.keys(themes)
}
-const cvStyleId = 'cv-style'
-
-export function renderThemeOn(name, el, data, primaryColor) {
- const theme = getTheme(name)
- el.innerHTML = renderTheme(theme.template, data)
-
- upsertStyleTag(cvStyleId, theme.style)
+export function getThemeDefaultColor(themeName) {
+ return themeColors[themeName]
+}
- document.documentElement.style.setProperty(varNamePrimaryColor, primaryColor)
+export function renderThemeOn(themeName, el, data, primaryColor) {
+ const renderFn = themes[themeName]
+ if (!renderFn) {
+ console.error(`Theme ${themeName} not found`)
+ return
+ }
+ renderFn(el, data, primaryColor)
}
diff --git a/src/themes/reorx/index.ejs b/src/themes/reorx/index.ejs
index 2a30151..7490f8e 100644
--- a/src/themes/reorx/index.ejs
+++ b/src/themes/reorx/index.ejs
@@ -372,25 +372,32 @@ function date(item, level) {
-<% if (hasItems(cv.publications)) { %>
-
-
- <% for (const item of cv.publications) { %>
-
-
-
<%= item.name %>
- <%- date(item, 2) %>
+<% if (cv.publications?.length) { %>
+
+ Publications
+ <% cv.publications.forEach(function(pub) { %>
+ <% console.log('[Debug] Template rendering publication:', pub) %>
+
+
<%= pub.name %>
+ <% if (pub.authors?.length) { %>
+ <% console.log('[Debug] Template rendering authors:', pub.authors) %>
+
+ <%= pub.authors.join('; ') %>
+
+ <% } %>
+
+ <% if (pub.venue || pub.releaseDate) { %>
+ <%= pub.venue %><%= pub.releaseDate ? (pub.venue ? ', ' : '') + fn.reformatDate(pub.releaseDate, 'YYYY') : '' %>
+ <% } %>
-
<%= item.publisher %>
- <%- linkInDiv(item.url) %>
- <% if (item.summary) { %>
-
<%- fn.renderMarkdown(item.summary) %>
+ <% if (pub.summary) { %>
+
<%- fn.renderMarkdown(pub.summary) %>
+ <% } %>
+ <% if (pub.url) { %>
+
<% } %>
- <% } %>
+ <% }) %>
<% } %>
diff --git a/src/themes/reorx/index.js b/src/themes/reorx/index.js
new file mode 100644
index 0000000..a6c8cb6
--- /dev/null
+++ b/src/themes/reorx/index.js
@@ -0,0 +1,13 @@
+import ejs from 'ejs';
+import { upsertStyleTag } from '../../lib/utils';
+import { getRenderData, varNamePrimaryColor } from '../data';
+import template from './index.ejs';
+import style from './index.scss?inline';
+
+export const defaultColor = '#aaaaaa' // matches :root { --color-primary } in index.scss
+
+export function renderTheme(el, data, primaryColor) {
+ el.innerHTML = ejs.render(template, getRenderData(data))
+ upsertStyleTag('cv-style', style)
+ document.documentElement.style.setProperty(varNamePrimaryColor, primaryColor)
+}
\ No newline at end of file
diff --git a/src/themes/reorx/index.scss b/src/themes/reorx/index.scss
index 88212d1..7705f81 100644
--- a/src/themes/reorx/index.scss
+++ b/src/themes/reorx/index.scss
@@ -224,3 +224,45 @@ section {
margin-bottom: 0;
}
}
+
+.publications {
+ .item {
+ margin-bottom: 1.5em;
+
+ .title {
+ font-weight: bold;
+ margin-bottom: 0.3em;
+ }
+
+ .authors {
+ color: var(--color-primary);
+ margin-bottom: 0.3em;
+ }
+
+ .publisher {
+ font-style: italic;
+ margin-bottom: 0.2em;
+ }
+
+ .date {
+ color: #666;
+ font-size: 0.9em;
+ margin-bottom: 0.2em;
+ }
+
+ .summary {
+ margin: 0.5em 0;
+ }
+
+ .url {
+ font-size: 0.9em;
+ a {
+ color: var(--color-primary);
+ text-decoration: none;
+ &:hover {
+ text-decoration: underline;
+ }
+ }
+ }
+ }
+}
diff --git a/vite.config.js b/vite.config.js
index d83935c..17a34e9 100644
--- a/vite.config.js
+++ b/vite.config.js
@@ -18,7 +18,6 @@ renderData.meta = {
description: data.basics.summary.replace('\n', ' '),
}
-
export default defineConfig({
build: {
outDir: outDir,
diff --git a/vite.config.site.js b/vite.config.site.js
index 2d5f344..28f6a5e 100644
--- a/vite.config.site.js
+++ b/vite.config.site.js
@@ -28,6 +28,14 @@ const renderData = {
export default defineConfig({
root: 'src',
+ optimizeDeps: {
+ include: [
+ '@json-editor/json-editor/dist/jsoneditor',
+ 'cash-dom',
+ 'object-path',
+ 'iconify-icon'
+ ]
+ },
build: {
// allows 'import.meta.glob' to work
target: 'esnext',