jekyll-theme-resume 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. checksums.yaml +7 -0
  2. data/403.html +6 -0
  3. data/404.html +6 -0
  4. data/500.html +6 -0
  5. data/CHANGELOG.md +498 -0
  6. data/CODE_OF_CONDUCT.md +92 -0
  7. data/LICENSE.txt +22 -0
  8. data/README.md +184 -0
  9. data/SECURITY.md +30 -0
  10. data/_config.sample.yml +329 -0
  11. data/_data/locales/ar.yml +85 -0
  12. data/_data/locales/de.yml +86 -0
  13. data/_data/locales/en.yml +84 -0
  14. data/_data/locales/es.yml +86 -0
  15. data/_data/locales/fr.yml +86 -0
  16. data/_data/locales/ur.yml +86 -0
  17. data/_data/social_networks.yml +83 -0
  18. data/_includes/analytics-body.html +12 -0
  19. data/_includes/analytics-head.html +36 -0
  20. data/_includes/avatar.html +84 -0
  21. data/_includes/dark-mode-toggle.html +202 -0
  22. data/_includes/data-loader.html +42 -0
  23. data/_includes/date-formatter.html +63 -0
  24. data/_includes/grouped-item-list.html +53 -0
  25. data/_includes/hreflang.html +58 -0
  26. data/_includes/language-switcher.html +72 -0
  27. data/_includes/print-social-links.html +23 -0
  28. data/_includes/resume-section.html +547 -0
  29. data/_includes/safe-url.html +21 -0
  30. data/_includes/shared-head.html +51 -0
  31. data/_includes/social-links.html +54 -0
  32. data/_includes/vendors/svg-icons/ATTRIBUTION.md +34 -0
  33. data/_includes/vendors/svg-icons/dev.svg +3 -0
  34. data/_includes/vendors/svg-icons/dribbble-symbol.svg +3 -0
  35. data/_includes/vendors/svg-icons/envelope.svg +4 -0
  36. data/_includes/vendors/svg-icons/facebook.svg +3 -0
  37. data/_includes/vendors/svg-icons/flickr.svg +4 -0
  38. data/_includes/vendors/svg-icons/github.svg +3 -0
  39. data/_includes/vendors/svg-icons/globe-1.svg +3 -0
  40. data/_includes/vendors/svg-icons/instagram.svg +4 -0
  41. data/_includes/vendors/svg-icons/linkedin.svg +3 -0
  42. data/_includes/vendors/svg-icons/medium.svg +3 -0
  43. data/_includes/vendors/svg-icons/phone.svg +13 -0
  44. data/_includes/vendors/svg-icons/pinterest.svg +3 -0
  45. data/_includes/vendors/svg-icons/postcard.svg +14 -0
  46. data/_includes/vendors/svg-icons/telegram.svg +3 -0
  47. data/_includes/vendors/svg-icons/whatsapp.svg +3 -0
  48. data/_includes/vendors/svg-icons/x.svg +3 -0
  49. data/_includes/vendors/svg-icons/youtube.svg +3 -0
  50. data/_layouts/default.html +65 -0
  51. data/_layouts/error.html +138 -0
  52. data/_layouts/profile.html +123 -0
  53. data/_layouts/resume.html +238 -0
  54. data/_plugins/error_pages_generator.rb +71 -0
  55. data/_plugins/json_resume_generator.rb +79 -0
  56. data/_plugins/resume_pages_generator.rb +71 -0
  57. data/_plugins/resume_validator.rb +40 -0
  58. data/_sass/_all-pages.scss +335 -0
  59. data/_sass/_base.scss +130 -0
  60. data/_sass/_dark-mode.scss +387 -0
  61. data/_sass/_layout.scss +116 -0
  62. data/_sass/_mixins.scss +136 -0
  63. data/_sass/_normalize.scss +379 -0
  64. data/_sass/_profile-page.scss +60 -0
  65. data/_sass/_resume-ltr.scss +436 -0
  66. data/_sass/_resume-rtl.scss +91 -0
  67. data/_sass/_variables.scss +45 -0
  68. data/assets/css/cv-ltr.scss +31 -0
  69. data/assets/css/cv-rtl.scss +36 -0
  70. data/assets/css/main.scss +8 -0
  71. data/assets/css/profile.scss +9 -0
  72. data/assets/favicon/resume/about.txt +6 -0
  73. data/assets/favicon/resume/android-chrome-192x192.png +0 -0
  74. data/assets/favicon/resume/android-chrome-512x512.png +0 -0
  75. data/assets/favicon/resume/apple-touch-icon.png +0 -0
  76. data/assets/favicon/resume/favicon-16x16.png +0 -0
  77. data/assets/favicon/resume/favicon-32x32.png +0 -0
  78. data/assets/favicon/resume/favicon.ico +0 -0
  79. data/assets/favicon/resume/site.webmanifest +1 -0
  80. data/bin/validate-resume +89 -0
  81. data/bin/verify +27 -0
  82. data/docs/README.md +89 -0
  83. data/docs/explanation/accessibility-decisions.md +27 -0
  84. data/docs/explanation/architecture.md +44 -0
  85. data/docs/explanation/dark-mode-approach.md +25 -0
  86. data/docs/explanation/data-driven-model.md +25 -0
  87. data/docs/explanation/multilingual-and-rtl-design.md +40 -0
  88. data/docs/how-to/add-a-language.md +57 -0
  89. data/docs/how-to/add-a-section.md +35 -0
  90. data/docs/how-to/add-a-social-platform.md +32 -0
  91. data/docs/how-to/add-a-test.md +34 -0
  92. data/docs/how-to/create-a-custom-layout.md +64 -0
  93. data/docs/how-to/enable-dark-mode.md +43 -0
  94. data/docs/how-to/link-translations-with-hreflang.md +25 -0
  95. data/docs/how-to/migrate-v0.9-to-v1.0.md +46 -0
  96. data/docs/how-to/override-locale-strings.md +51 -0
  97. data/docs/how-to/override-sass-partials.md +26 -0
  98. data/docs/how-to/proof-built-html.md +22 -0
  99. data/docs/how-to/publish-json-resume.md +38 -0
  100. data/docs/how-to/show-language-proficiency-in-header.md +13 -0
  101. data/docs/how-to/switch-resume-versions.md +42 -0
  102. data/docs/how-to/troubleshoot-builds.md +39 -0
  103. data/docs/how-to/validate-in-ci.md +38 -0
  104. data/docs/how-to/verify-accessibility.md +28 -0
  105. data/docs/reference/accessibility-coverage.md +39 -0
  106. data/docs/reference/config.md +378 -0
  107. data/docs/reference/data-schemas.md +529 -0
  108. data/docs/reference/glossary.md +39 -0
  109. data/docs/reference/includes.md +197 -0
  110. data/docs/reference/json-resume-fields.md +148 -0
  111. data/docs/reference/layouts.md +144 -0
  112. data/docs/reference/locale-keys.md +76 -0
  113. data/docs/reference/repository-map.md +83 -0
  114. data/docs/reference/sass-tokens.md +279 -0
  115. data/docs/reference/testing-suites.md +62 -0
  116. data/docs/reference/validator-cli.md +203 -0
  117. data/docs/tutorials/getting-started.md +173 -0
  118. data/lib/jekyll-theme-resume/json_resume_exporter.rb +334 -0
  119. data/lib/jekyll-theme-resume/resume_validator.rb +853 -0
  120. data/lib/jekyll-theme-resume/schemas/LICENSE.md +21 -0
  121. data/lib/jekyll-theme-resume/schemas/json_resume_v1.0.0.json +500 -0
  122. data/lib/jekyll-theme-resume.rb +21 -0
  123. metadata +371 -0
@@ -0,0 +1,238 @@
1
+ {%- assign lang = page.lang | default: site.default_lang | default: 'en' -%}
2
+ {%- assign locale = site.data.locales[lang] | default: site.data.locales[site.default_lang] -%}
3
+ {%- assign lang_cfg = site.languages[lang] -%}
4
+ {% include data-loader.html path=lang_cfg.data_path %}
5
+ <!DOCTYPE html>
6
+ <html lang="{{ lang | escape }}" dir="{{ locale.direction }}">
7
+ <!-- This page was last updated on {{ "now" | date: "%Y-%m-%d %H:%M:%S" }}Z -->
8
+
9
+ <head>
10
+ {%- comment -%}
11
+ Head Assets:
12
+ - shared-head: Shared charset, viewport, SEO base, and favicon links
13
+ - Locale web font (locale.font_url) and locale-driven CSS (assets/css/cv-{{ locale.direction }}.css)
14
+ - seo: Jekyll SEO tag for social graph, search indexing, and canonical URLs
15
+ - analytics-head: Google Analytics / GTM tracking scripts (optional)
16
+ {%- endcomment -%}
17
+ {% include shared-head.html %}
18
+
19
+ {% if site.disable_google_fonts != true and site.resume_theme != 'no-custom-fonts' %}
20
+ {% if locale.font_url != "" %}
21
+ <!-- Locale web font, from _data/locales/{{ lang }}.yml -->
22
+ <link rel="preconnect" href="https://fonts.googleapis.com">
23
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
24
+ <link href="{{ locale.font_url }}" rel="stylesheet">
25
+ {% else %}
26
+ <!-- Default web font (Lora + Open Sans) for locales without a dedicated font_url -->
27
+ <link href='https://fonts.googleapis.com/css?family=Lora:400,700%7COpen+Sans:400,300,800,700' rel='stylesheet' type='text/css'>
28
+ {% endif %}
29
+ {% endif %}
30
+
31
+ <style>
32
+ :root {
33
+ {% if locale.font_family != "" %}--font-locale: {{ locale.font_family }};{% endif %}
34
+ --line-height-locale: {{ locale.line_height }};
35
+ }
36
+ </style>
37
+
38
+ <link rel="stylesheet" type="text/css" href="{{ 'assets/css/cv-' | append: locale.direction | append: '.css' | relative_url }}">
39
+
40
+ {% include hreflang.html %}
41
+
42
+ {% seo %}
43
+
44
+ {% include analytics-head.html %}
45
+ </head>
46
+
47
+ <body class="theme-{% if site.resume_theme %}{{ site.resume_theme }}{% endif %}">
48
+ <!-- Accessible skip-to-content link (WCAG 2.4.1) -->
49
+ <a href="#main-content" class="skip-link no-print">{{ locale.ui.skip_to_content }}</a>
50
+
51
+ <!-- Analytics body snippet (noscript, dataLayer, etc.) -->
52
+ {% include analytics-body.html %}
53
+
54
+ <div class="wrapper" itemscope itemtype="https://schema.org/Person">
55
+ {% include dark-mode-toggle.html %}
56
+ {% include language-switcher.html %}
57
+
58
+ {%- comment -%}
59
+ Schema.org Microdata & Live Contact Protection:
60
+ When site.enable_live is true, phone_live and email_live are used for privacy.
61
+ Fallback to standard phone/email if live versions are not provided.
62
+ {%- endcomment -%}
63
+ {% if site.enable_live == true and site.contact_info.phone_live %}
64
+ <meta itemprop="telephone" content="{{ site.contact_info.phone_live }}" />
65
+ {% elsif site.contact_info.phone %}
66
+ <meta itemprop="telephone" content="{{ site.contact_info.phone }}" />
67
+ {% endif %}
68
+
69
+ {% if site.enable_live == true and site.contact_info.email_live %}
70
+ <meta itemprop="email" content="{{ site.contact_info.email_live }}" />
71
+ {% elsif site.contact_info.email %}
72
+ <meta itemprop="email" content="{{ site.contact_info.email }}" />
73
+ {% endif %}
74
+
75
+ {% if lang_cfg.address %}
76
+ <meta itemprop="address" content="{{ lang_cfg.address }}" />
77
+ {% endif %}
78
+
79
+ <header class="page-header" role="banner">
80
+
81
+ {% if site.resume_avatar == true %}
82
+ <!-- You can turn off the avatar in _config.yml by setting resume_avatar to false -->
83
+ {% include avatar.html lang=lang %}
84
+ {% endif %}
85
+
86
+ <!-- Your name is defined in _config.yml under languages.{{ lang }}.name -->
87
+ <h1 class="header-name" itemprop="name">
88
+ {{ lang_cfg.name }}
89
+ </h1>
90
+
91
+ {%- comment -%}
92
+ Contact Information Bar (Feature 2.8, icon-first for every direction):
93
+ Displays contact channels (phone, email, address, date of birth, languages).
94
+ When site.enable_live is true, sanitized live contact points are rendered.
95
+ Phone numbers and email addresses are wrapped with dir="ltr" to prevent
96
+ bidirectional punctuation distortion in RTL locales, and are harmless no-ops in LTR ones.
97
+ {%- endcomment -%}
98
+ {% if site.display_header_contact_info == true %}
99
+ <!-- To display contact info here, change `display_header_contact_info` value in _config.yml to true -->
100
+ <div class="header-contact-info">
101
+ <p>
102
+ {%- assign contact_phone = site.contact_info.phone -%}
103
+ {%- if site.enable_live == true and site.contact_info.phone_live -%}
104
+ {%- assign contact_phone = site.contact_info.phone_live -%}
105
+ {%- endif -%}
106
+ {% if contact_phone %}
107
+ {% include vendors/svg-icons/phone.svg %}&nbsp;<a href="tel:{{ contact_phone | remove: ' ' | remove: '-' | remove: '(' | remove: ')' }}" dir="ltr" style="text-decoration: none; color: inherit;">{{ contact_phone }}</a>
108
+ {% endif %}
109
+
110
+ {%- assign contact_email_display = site.contact_info.email -%}
111
+ {%- if site.enable_live == true and site.contact_info.email_live -%}
112
+ {%- assign contact_email_display = site.contact_info.email_live -%}
113
+ {%- endif -%}
114
+ {% if contact_email_display %}
115
+ &nbsp;&nbsp;{% include vendors/svg-icons/envelope.svg %}&nbsp;<a href="mailto:{{ contact_email_display }}" dir="ltr" style="text-decoration: none; color: inherit;">{{ contact_email_display }}</a>
116
+ {% endif %}
117
+
118
+ {% if lang_cfg.address %}
119
+ &nbsp;&nbsp;{% include vendors/svg-icons/postcard.svg %}&nbsp;{{ lang_cfg.address }}
120
+ {% endif %}
121
+
122
+ {% if site.contact_info.dob %}
123
+ &nbsp;&nbsp;{{ locale.ui.date_of_birth }}&nbsp;{% include date-formatter.html date=site.contact_info.dob style="MDY" lang=lang %}
124
+ {% endif %}
125
+ </p>
126
+
127
+ {%- comment -%}
128
+ Active Languages Header Summary:
129
+ Filters languages where active: true and renders a dedicated centered line,
130
+ joined with the locale's list_separator (e.g. Arabic comma "، ").
131
+ {%- endcomment -%}
132
+ {% assign active_languages = resume_data.languages | where: "active", true %}
133
+ {% if site.resume_section.lang_header and active_languages.size > 0 %}
134
+ <p class="header-languages">
135
+ <strong>{{ locale.ui.languages }}</strong>
136
+ {% for language in active_languages %}
137
+ {{ language.language }} ({{ language.descrp_short }}){% unless forloop.last %}{{ locale.ui.list_separator }}{% endunless %}
138
+ {% endfor %}
139
+ </p>
140
+ {% endif %}
141
+ </div>
142
+ {% endif %}
143
+
144
+ <div class="title-bar no-print">
145
+ <!-- Your title is defined in _config.yml under languages.{{ lang }}.resume_title -->
146
+ <h2 class="header-title" itemprop="jobTitle">{{ lang_cfg.resume_title }}</h2>
147
+
148
+ <!-- This is the markup for the icon links; moved out to an include because it's very verbose, and you shouldn't ever need to edit the markup (unless you want to re-order the icons); if you want to customize which links appear, define them in the _config.yml file -->
149
+ {% if site.social_links %}
150
+ <!-- and guess where these are defined? Yup, you guessed it: the _config.yml file -->
151
+ <ul class="social-links">
152
+ {% include social-links.html %}
153
+ </ul>
154
+ {% endif %}
155
+ </div>
156
+
157
+ {% if lang_cfg.header_intro == true %}
158
+ <div class="executive-summary" itemprop="description">
159
+ <p>{{ resume_data.header.intro }}</p>
160
+ </div>
161
+ {% endif %}
162
+
163
+ {%- comment -%}
164
+ Job Seeking / Availability Call-to-Action:
165
+ - true: Displays localized "Contact me" mailto CTA.
166
+ - false: Displays localized "I'm not looking for work" badge.
167
+ - nil: Hidden entirely.
168
+ {%- endcomment -%}
169
+ {% if site.resume_looking_for_work == true %}
170
+ {% assign contact_email = site.contact_info.email %}
171
+ {% if site.enable_live == true and site.contact_info.email_live %}
172
+ {% assign contact_email = site.contact_info.email_live %}
173
+ {% endif %}
174
+ <a href="mailto:{{ contact_email }}" class="contact-button no-print" itemprop="email">{{ locale.ui.contact_me }}</a>
175
+ {% elsif site.resume_looking_for_work == false %}
176
+ <a class="contact-button not-looking no-print">{{ locale.ui.not_looking_for_work }}</a>
177
+ {% else %}
178
+ {% endif %}
179
+ </header>
180
+
181
+ <!-- Main resume content landmark (WCAG 1.3.1 / 2.4.1) -->
182
+ <main id="main-content" class="resume-main-content" role="main" tabindex="-1">
183
+ {% comment %}
184
+ =============================================================================
185
+ DYNAMIC SECTION RENDERING
186
+ =============================================================================
187
+ Loop through resume_section_order from _config.yml and render each section
188
+ through the locale-driven dispatcher. This allows you to customize section
189
+ order without editing the layout.
190
+ {% endcomment %}
191
+ {%- for section_name in site.resume_section_order -%}
192
+ {% include resume-section.html section_name=section_name lang=lang %}
193
+ {%- endfor -%}
194
+
195
+ {% if site.resume_print_social_links %}
196
+ <!-- begin Print Social Links -->
197
+ <section class="content-section print-only">
198
+
199
+ <header class="section-header">
200
+ <h2>{{ locale.ui.social_links }}</h2>
201
+ </header>
202
+
203
+ <div class="resume-item">
204
+ {% include print-social-links.html %}
205
+ </div>
206
+
207
+ </section>
208
+ <!-- end Print Social Links -->
209
+ {% endif %}
210
+ </main>
211
+
212
+ <footer class="page-footer no-print" role="contentinfo">
213
+ <p class="footer-line">
214
+ {{ locale.ui.page_last_generated_on }} {{ "now" | date: "%Y-%m-%d" }} {{ locale.ui.at }} {{ "now" | date: "%H:%M:%S" }}Z.
215
+ </p>
216
+ <p class="copyright">{{ site.title }} &copy; 2018 - {{ "now" | date: "%Y" }}</p>
217
+ </footer>
218
+
219
+ {%- comment -%}
220
+ Print-Only Footer:
221
+ When site.enable_live == false, appends the canonical permalink of the online resume
222
+ to printed versions/PDF exports for offline verification.
223
+ {%- endcomment -%}
224
+ {% if site.enable_live == false %}
225
+ <footer class="page-footer print-only" role="contentinfo">
226
+ <p class="footer-line">
227
+ {{ locale.ui.cv_last_generated_on }} {{ "now" | date: "%Y-%m-%d" }} {{ locale.ui.at }} {{ "now" | date: "%H:%M:%S" }}Z.
228
+ <br>
229
+ <a href="{{ page.url | replace:'index.html','' | prepend: site.baseurl | prepend: site.url }}" target="_blank" rel="noopener noreferrer" style="font-weight: 100; color: #999;">
230
+ {{ page.url | replace:'index.html','' | prepend: site.baseurl | prepend: site.url }}
231
+ </a>
232
+ </p>
233
+ </footer>
234
+ {% endif %}
235
+ </div>
236
+
237
+ </body>
238
+ </html>
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JekyllThemeResume
4
+ # Programmatically synthesizes standard HTTP error pages (404, 403, 500)
5
+ # into site.pages if they are not already defined by the site.
6
+ #
7
+ # WHY THIS IS NECESSARY:
8
+ # Jekyll gem-based themes only distribute assets, _layouts, _includes, _sass,
9
+ # and _data. Root-level HTML files in a gem theme are NOT automatically copied
10
+ # or rendered into the consuming site's destination directory (_site/).
11
+ #
12
+ # HOW IT WORKS:
13
+ # This generator executes during the Jekyll build pipeline. For each error page
14
+ # defined in ERROR_PAGES, it checks whether the consuming site already provides
15
+ # an override. If no override exists, it creates an in-memory virtual page
16
+ # (Jekyll::PageWithoutAFile) and adds it to site.pages so Jekyll compiles it
17
+ # using _layouts/error.html.
18
+ class ErrorPagesGenerator < Jekyll::Generator
19
+ # safe true: Allows plugin execution in Jekyll safe mode / GitHub Pages environments
20
+ safe true
21
+ # priority :low: Ensures all physical pages in the consuming site are parsed and
22
+ # added to site.pages first, allowing our collision checks to be fully accurate.
23
+ priority :low
24
+
25
+ # Standard HTTP error codes, target output filenames, and default bilingual titles.
26
+ ERROR_PAGES = {
27
+ "404" => {
28
+ "file" => "404.html",
29
+ "title" => "404 - Page Not Found"
30
+ },
31
+ "403" => {
32
+ "file" => "403.html",
33
+ "title" => "403 - Access Forbidden"
34
+ },
35
+ "500" => {
36
+ "file" => "500.html",
37
+ "title" => "500 - Internal Server Error"
38
+ }
39
+ }.freeze
40
+
41
+ def generate(site)
42
+ ERROR_PAGES.each do |code, info|
43
+ filename = info["file"]
44
+ # Skip if page already exists in site.pages or in site source files
45
+ #
46
+ # THREE-TIER COLLISION PREVENTION:
47
+ # 1. Check in-memory site.pages for existing page name or permalink URL
48
+ next if site.pages.any? { |p| p.name == filename || p.url == "/#{filename}" }
49
+ # 2. Check if a physical file exists at the root of the consuming site
50
+ next if File.exist?(site.in_source_dir(filename))
51
+ # 3. Check if a physical file exists in the _pages/ collection directory
52
+ next if File.exist?(site.in_source_dir("_pages", filename))
53
+
54
+ # Synthesize virtual page in memory without requiring a physical file on disk.
55
+ # Arguments: site, base directory, sub-directory (""), file name
56
+ page = Jekyll::PageWithoutAFile.new(site, site.source, "", filename)
57
+ page.content = ""
58
+ # Assign Front Matter metadata consumed by _layouts/error.html:
59
+ page.data["layout"] = "error"
60
+ page.data["code"] = code
61
+ page.data["title"] = info["title"]
62
+ page.data["permalink"] = "/#{filename}"
63
+ # Exclude synthetic error pages from sitemap generators (e.g. jekyll-sitemap)
64
+ page.data["sitemap"] = false
65
+
66
+ # Append virtual page to site.pages so Jekyll renders and writes it to _site/
67
+ site.pages << page
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../lib/jekyll-theme-resume/json_resume_exporter"
4
+ require_relative "../lib/jekyll-theme-resume/resume_validator"
5
+
6
+ module JekyllThemeResume
7
+ class JsonResumePage < Jekyll::PageWithoutAFile
8
+ def initialize(site, route, document)
9
+ super(site, site.source, File.dirname(route).delete_prefix("/"), "resume.json")
10
+ self.content = "#{JSON.pretty_generate(document)}\n"
11
+ data.merge!("layout" => nil, "permalink" => route, "sitemap" => false)
12
+ end
13
+
14
+ # Jekyll::Site uses Renderer directly, not Page#render.
15
+ def render_with_liquid?
16
+ false
17
+ end
18
+
19
+ def place_in_layout?
20
+ false
21
+ end
22
+ end
23
+
24
+ class JsonResumeGenerator < Jekyll::Generator
25
+ safe true
26
+ priority :lowest
27
+
28
+ def generate(site)
29
+ site.config["json_resume_routes"] = {}
30
+ options = site.config["json_resume"].is_a?(Hash) ? site.config["json_resume"] : {}
31
+ languages = site.config["languages"]
32
+ return if options["enabled"] == false || !languages.is_a?(Hash)
33
+
34
+ selected_languages(languages, options).each do |lang|
35
+ unless ResumeValidator::LANG_KEY_REGEX.match?(lang.to_s)
36
+ Jekyll.logger.warn "JSON Resume:", "invalid language route; skipping export"
37
+ next
38
+ end
39
+ route = "/#{lang}/resume.json"
40
+ next if collision?(site, route)
41
+
42
+ document = JsonResumeExporter.export(site, lang)
43
+ next unless document
44
+
45
+ site.pages << JsonResumePage.new(site, route, document)
46
+ site.config["json_resume_routes"][lang] = route
47
+ next unless lang == (site.config["default_lang"] || "en") && options["root_export"] != false
48
+ next if collision?(site, "/resume.json")
49
+
50
+ site.pages << JsonResumePage.new(site, "/resume.json", document)
51
+ end
52
+ end
53
+
54
+ private
55
+
56
+ def selected_languages(languages, options)
57
+ requested = options["languages"]
58
+ return languages.keys if requested.nil? || requested == []
59
+
60
+ unless requested.is_a?(Array)
61
+ Jekyll.logger.warn "JSON Resume:", "languages must be an array; skipping exports"
62
+ return []
63
+ end
64
+ requested.uniq.select do |lang|
65
+ known = languages.key?(lang)
66
+ Jekyll.logger.warn "JSON Resume:", "unknown export language '#{lang}'; ignored" unless known
67
+ known
68
+ end
69
+ end
70
+
71
+ def collision?(site, route)
72
+ target = File.expand_path(route.delete_prefix("/"), site.dest)
73
+ resources = site.pages + site.static_files + site.collections.values.flat_map(&:docs)
74
+ occupied = resources.any? { |item| File.expand_path(item.destination(site.dest)) == target }
75
+ Jekyll.logger.warn "JSON Resume:", "#{route}: destination occupied; keeping existing resource" if occupied
76
+ occupied
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../lib/jekyll-theme-resume/resume_validator"
4
+
5
+ module JekyllThemeResume
6
+ # Synthesizes a CV (layout: resume) and profile (layout: profile) page for every
7
+ # languages.<lang> entry in _config.yml that doesn't already have a hand-authored one,
8
+ # the same way ErrorPagesGenerator synthesizes 404/403/500 pages (see
9
+ # error_pages_generator.rb). t_id matches the documented convention (t_id: resume /
10
+ # t_id: profile) so hreflang.html and language-switcher.html match generated and
11
+ # hand-authored pages identically.
12
+ class ResumePagesGenerator < Jekyll::Generator
13
+ safe true
14
+ # priority :low: run after physical pages are parsed, so collision checks are accurate.
15
+ priority :low
16
+
17
+ def generate(site)
18
+ languages = site.config["languages"]
19
+ return unless languages.is_a?(Hash)
20
+
21
+ default_lang = site.config["default_lang"] || "en"
22
+
23
+ languages.each do |lang, lang_cfg|
24
+ unless ResumeValidator::LANG_KEY_REGEX.match?(lang.to_s)
25
+ Jekyll.logger.warn "ResumePagesGenerator:",
26
+ "skipping language #{lang.to_s.inspect}: keys may only use letters, digits, '-' and '_'."
27
+ next
28
+ end
29
+
30
+ lang_cfg ||= {}
31
+ synthesize_or_warn(site, "resume", lang, lang_cfg["url"], lang_cfg)
32
+
33
+ profile_url = lang == default_lang ? "/" : "/#{lang}/"
34
+ synthesize_or_warn(site, "profile", lang, profile_url, lang_cfg)
35
+ end
36
+ end
37
+
38
+ private
39
+
40
+ def synthesize_or_warn(site, layout, lang, permalink, lang_cfg)
41
+ return if existing_page?(site, layout, lang, permalink)
42
+
43
+ unless permalink && auto_generate?(site, lang_cfg)
44
+ Jekyll.logger.warn "ResumePagesGenerator:",
45
+ "no #{layout} page for language '#{lang}' - auto-generation is " \
46
+ "disabled or languages.#{lang}.url is missing, and no " \
47
+ "hand-authored page exists. This language will not be built."
48
+ return
49
+ end
50
+
51
+ page = Jekyll::PageWithoutAFile.new(site, site.source, "", "#{layout}-#{lang}.html")
52
+ page.content = ""
53
+ page.data["layout"] = layout
54
+ page.data["lang"] = lang
55
+ page.data["t_id"] = layout
56
+ page.data["permalink"] = permalink
57
+ site.pages << page
58
+ end
59
+
60
+ def existing_page?(site, layout, lang, permalink)
61
+ site.pages.any? { |p| p.data["layout"] == layout && p.data["lang"] == lang } ||
62
+ (permalink && site.pages.any? { |p| p.url == permalink })
63
+ end
64
+
65
+ def auto_generate?(site, lang_cfg)
66
+ return lang_cfg["auto_generate_pages"] unless lang_cfg["auto_generate_pages"].nil?
67
+
68
+ site.config["resume_auto_generate_pages"] != false
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../lib/jekyll-theme-resume/resume_validator"
4
+
5
+ module JekyllThemeResume
6
+ # Jekyll Generator that validates resume YAML data during Jekyll build (on by default).
7
+ #
8
+ # _config.yml switches:
9
+ # validate_resume: false # Opt out of validation entirely
10
+ # validate_resume_strict: true # Abort the build on validation errors
11
+ class ResumeValidatorGenerator < Jekyll::Generator
12
+ safe true
13
+ priority :low
14
+
15
+ def generate(site)
16
+ return if site.config["validate_resume"] == false
17
+
18
+ data_dir = site.config["data_dir"] || "_data"
19
+ full_data_path = site.in_source_dir(data_dir)
20
+
21
+ # Check if the data directory or language subdirectories exist
22
+ unless Dir.exist?(full_data_path)
23
+ Jekyll.logger.warn("ResumeValidator:", "Configured data directory '#{full_data_path}' not found. Skipping.")
24
+ return
25
+ end
26
+
27
+ # Hand over the already-parsed config so languages come from `languages:` without re-reading disk.
28
+ validator = ResumeValidator.new(full_data_path, config: site.config)
29
+ strict = site.config["validate_resume_strict"] == true
30
+ fail_warnings = site.config["validate_resume_fail_on_warnings"] == true
31
+ validator.validate(fail_on_warnings: fail_warnings)
32
+
33
+ return unless strict && (validator.errors.any? || (fail_warnings && validator.warnings.any?))
34
+
35
+ msg = "Resume validation failed with #{validator.errors.size} error(s)"
36
+ msg += " and #{validator.warnings.size} warning(s)" if fail_warnings
37
+ raise Jekyll::Errors::FatalException, "#{msg} in strict mode."
38
+ end
39
+ end
40
+ end