al_folio_cv 1.0.0 → 1.0.1

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e37c2fee391369069d49478404ddc88f4b69337992b96d7cba2ec39472c6c16c
4
- data.tar.gz: 49c29ef1bbaccbff167e8d4a86dfad41efef7c5ec73beab5239cd81a1b619de2
3
+ metadata.gz: 735f4f1219fb50c22009f42027dd424035eff20e82146b91f0f94670a3df1951
4
+ data.tar.gz: '0428b2b70e9be220422a77c22f6fa84f1b84e0bbdc1bf0e01628f03ab26aa948'
5
5
  SHA512:
6
- metadata.gz: 347cd226c6de23f5cc8797594643f61d02a274ad2ae0d011ceaf6b28ac04334c48dbe33deb69c5948c073a0ee7070b77a4a15a5314e66e810a1c3f0dccd6093c
7
- data.tar.gz: 8d3ece2ff37d0298b3c2a9ea60fb261ed485d7a3713b7a76169690c40fef87ae12fb586b35e98f913b571cda5436958c37b074611fbcd11facca206b64b2c934
6
+ metadata.gz: 7c6b922f64ad0b897b5f3664b27cbe88517007fdc0760cdf246e297dc70b206e8ac52581097316c63fc4703c19b6471b430210b328f9a9f7ec528ab50fe0d1ac
7
+ data.tar.gz: 5dd93c83a958cc31996e325b2f0d40cca98de5c1f708bb5314870dc723d51cbbf7801461fa8bd60c6b717d05d0547f99ba0c6c0fb657a24a6d89e7bd2854df40
data/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.0.1
4
+
5
+ - Sort experience, volunteering, and education entries by date instead of rendering them in source order, so volunteering is no longer always appended after work history (#3, ports `alshedivat/al-folio#3272`). Ordering is handled by the new `al_cv_sort_by_date` Liquid filter, which understands both RenderCV and JSONResume key names, partial dates (`2020`, `2020-06`), YAML date objects, textual `present` end dates, and undated entries.
6
+ - Strip captured dates and locations in the experience and education entry templates (#2, ports `alshedivat/al-folio#3537`). An entry with no end date now renders `Present` instead of a dangling separator, an entry with no dates renders no date badge, and an entry with no location no longer renders a location row containing just the map pin icon.
7
+
3
8
  ## 1.0.0
4
9
 
5
10
  - Initial CV extraction from `al_folio_core`.
data/README.md CHANGED
@@ -1,22 +1,20 @@
1
1
  # al-folio-cv
2
2
 
3
- CV rendering plugin for al-folio v1.x.
3
+ `al_folio_cv` is the CV rendering plugin for `al-folio` v1.x.
4
4
 
5
5
  ## What it provides
6
6
 
7
- - Shared CV rendering templates used by `layout: cv`
7
+ - Shared CV templates used by `layout: cv`
8
8
  - CV helper Liquid tag: `{% al_folio_cv_render %}`
9
9
  - Packaged stylesheet at `/assets/css/al-folio-cv.css`
10
10
 
11
- ## Usage
12
-
13
- Add the gem and plugin:
11
+ ## Installation
14
12
 
15
13
  ```ruby
16
14
  gem 'al_folio_cv'
17
15
  ```
18
16
 
19
- ```yml
17
+ ```yaml
20
18
  plugins:
21
19
  - al_folio_cv
22
20
  al_folio:
@@ -26,3 +24,12 @@ al_folio:
26
24
  ```
27
25
 
28
26
  `al_folio_core` delegates `layout: cv` rendering to this plugin.
27
+
28
+ ## Ecosystem context
29
+
30
+ - Starter examples/docs live in `al-folio`.
31
+ - CV runtime and rendering behavior are owned here.
32
+
33
+ ## Contributing
34
+
35
+ CV layout/rendering changes should be proposed in this repository.
@@ -0,0 +1,142 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "date"
4
+
5
+ module AlFolioCv
6
+ # Orders CV entries the way a CV is normally read: most recent first, with
7
+ # ongoing entries at the top.
8
+ #
9
+ # CV data reaches the templates from two sources that disagree on key names
10
+ # (RenderCV uses `start_date`/`end_date`, JSONResume uses
11
+ # `startDate`/`endDate`) and both allow partial dates ("2020", "2020-06"),
12
+ # textual end dates ("present") and entries with no dates at all. Everything
13
+ # here is therefore total: an unrecognised value is treated as "no date"
14
+ # rather than raising.
15
+ module DateSorting
16
+ START_KEYS = %w[start_date startDate].freeze
17
+ END_KEYS = %w[end_date endDate].freeze
18
+ POINT_KEYS = %w[date releaseDate].freeze
19
+
20
+ ONGOING_VALUES = %w[present current ongoing now].freeze
21
+
22
+ # "2020", "2020-06", "2020-06-01" and the "/" and "." separated variants.
23
+ PARTIAL_DATE = %r{\A(\d{4})(?:[-/.](\d{1,2}))?(?:[-/.](\d{1,2}))?\z}
24
+ # Last-resort fallback for free-form values such as "Fall 2019".
25
+ BARE_YEAR = /(?<!\d)(\d{4})(?!\d)/
26
+
27
+ ONGOING_RANK = Float::INFINITY
28
+ UNDATED_RANK = -Float::INFINITY
29
+
30
+ module_function
31
+
32
+ # Returns a new array ordered by end date descending, then start date
33
+ # descending. Ongoing entries come first, undated entries last, and the
34
+ # original order breaks ties so the sort is stable.
35
+ def sort(entries)
36
+ return entries unless entries.is_a?(Array)
37
+
38
+ entries.each_with_index
39
+ .map { |entry, index| [entry, sort_key(entry, index)] }
40
+ .sort_by(&:last)
41
+ .map(&:first)
42
+ end
43
+
44
+ def sort_key(entry, index)
45
+ start_rank, end_rank = bounds(entry)
46
+ [-end_rank, -start_rank, index]
47
+ end
48
+
49
+ # Resolves an entry to a [start_rank, end_rank] pair of comparable numbers.
50
+ def bounds(entry)
51
+ start_raw = first_present(entry, START_KEYS)
52
+ end_raw = first_present(entry, END_KEYS)
53
+ point_raw = first_present(entry, POINT_KEYS)
54
+
55
+ start_rank = parse(start_raw, :start)
56
+
57
+ end_rank =
58
+ if ongoing?(end_raw)
59
+ ONGOING_RANK
60
+ elsif end_raw
61
+ parse(end_raw, :end)
62
+ elsif start_rank
63
+ # A start date with no end date means the entry is still running,
64
+ # which is also how the templates render it ("2020 - Present"). A
65
+ # standalone `date` is a point in time and never counts as ongoing.
66
+ ONGOING_RANK
67
+ else
68
+ parse(point_raw, :end)
69
+ end
70
+
71
+ [start_rank || parse(point_raw, :start) || UNDATED_RANK, end_rank || UNDATED_RANK]
72
+ end
73
+
74
+ def ongoing?(value)
75
+ value.is_a?(String) && ONGOING_VALUES.include?(value.strip.downcase)
76
+ end
77
+
78
+ # Turns a date value into an integer of the form YYYYMMDD. Missing
79
+ # components are padded to the start or the end of the period so that a
80
+ # year-only end date ("2020") sorts after a mid-year one ("2020-06").
81
+ def parse(value, boundary)
82
+ case value
83
+ when nil then nil
84
+ when Date, DateTime, Time then compose(value.year, value.month, value.day)
85
+ when Numeric then compose_partial(value.to_i, nil, nil, boundary)
86
+ when String, Symbol
87
+ text = value.to_s.strip
88
+ return nil if text.empty?
89
+
90
+ if (match = PARTIAL_DATE.match(text))
91
+ compose_partial(match[1].to_i, match[2]&.to_i, match[3]&.to_i, boundary)
92
+ elsif (match = BARE_YEAR.match(text))
93
+ compose_partial(match[1].to_i, nil, nil, boundary)
94
+ end
95
+ # Anything else (nested hashes, arrays, ...) counts as "no date"; its
96
+ # `to_s` could otherwise contribute a stray four-digit number.
97
+ end
98
+ end
99
+
100
+ def compose_partial(year, month, day, boundary)
101
+ if boundary == :end
102
+ compose(year, month || 12, day || 31)
103
+ else
104
+ compose(year, month || 1, day || 1)
105
+ end
106
+ end
107
+
108
+ def compose(year, month, day)
109
+ (year.to_i * 10_000) + (clamp(month, 1, 12) * 100) + clamp(day, 1, 31)
110
+ end
111
+
112
+ def clamp(value, min, max)
113
+ value = value.to_i
114
+ return min if value < min
115
+ return max if value > max
116
+
117
+ value
118
+ end
119
+
120
+ # First non-nil, non-blank value among the given keys.
121
+ def first_present(entry, keys)
122
+ keys.each do |key|
123
+ value = lookup(entry, key)
124
+ next if value.nil?
125
+ next if value.is_a?(String) && value.strip.empty?
126
+
127
+ return value
128
+ end
129
+ nil
130
+ end
131
+
132
+ def lookup(entry, key)
133
+ case entry
134
+ when Hash then entry[key].nil? ? entry[key.to_sym] : entry[key]
135
+ else
136
+ return nil unless entry.respond_to?(:[]) && entry.respond_to?(:key?)
137
+
138
+ entry[key]
139
+ end
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "date_sorting"
4
+
5
+ module AlFolioCv
6
+ # Liquid filters made available to CV templates.
7
+ module Filters
8
+ # Orders CV entries most recent first. Non-array input is passed through
9
+ # unchanged so the filter is safe to chain onto an optional section.
10
+ def al_cv_sort_by_date(input)
11
+ AlFolioCv::DateSorting.sort(input)
12
+ end
13
+ end
14
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AlFolioCv
4
- VERSION = "1.0.0"
4
+ VERSION = "1.0.1"
5
5
  end
data/lib/al_folio_cv.rb CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "jekyll"
4
4
  require_relative "al_folio_cv/version"
5
+ require_relative "al_folio_cv/filters"
5
6
 
6
7
  module AlFolioCv
7
8
  PLUGIN_ROOT = File.expand_path("..", __dir__)
@@ -46,6 +47,7 @@ module AlFolioCv
46
47
  end
47
48
 
48
49
  Liquid::Template.register_tag("al_folio_cv_render", AlFolioCv::RenderTag)
50
+ Liquid::Template.register_filter(AlFolioCv::Filters)
49
51
 
50
52
  Jekyll::Hooks.register :site, :after_init do |site|
51
53
  next unless site.respond_to?(:includes_load_paths)
@@ -10,12 +10,15 @@
10
10
  <li class="list-group-item">
11
11
  <div class="row">
12
12
  <div class="col-xs-2 col-sm-2 col-md-2 text-center date-column">
13
+ {% comment %} `capture` keeps the surrounding whitespace, so strip before testing for emptiness {% endcomment %}
13
14
  {% capture start_date %}
14
15
  {% if entry.start_date %}{{ entry.start_date }}{% elsif entry.startDate %}{{ entry.startDate }}{% endif %}
15
16
  {% endcapture %}
17
+ {% assign start_date = start_date | strip %}
16
18
  {% capture end_date %}
17
19
  {% if entry.end_date %}{{ entry.end_date }}{% elsif entry.endDate %}{{ entry.endDate }}{% endif %}
18
20
  {% endcapture %}
21
+ {% assign end_date = end_date | strip %}
19
22
 
20
23
  {% if start_date != '' %}
21
24
  {% comment %} Extract year only from dates like "1933-01-01" or just use "1933" as is {% endcomment %}
@@ -38,6 +41,7 @@
38
41
  {% capture location %}
39
42
  {% if entry.location %}{{ entry.location }}{% endif %}
40
43
  {% endcapture %}
44
+ {% assign location = location | strip %}
41
45
  {% if location != '' %}
42
46
  <tr>
43
47
  <td>
@@ -10,12 +10,15 @@
10
10
  <li class="list-group-item">
11
11
  <div class="row">
12
12
  <div class="col-xs-2 col-sm-2 col-md-2 text-center date-column">
13
+ {% comment %} `capture` keeps the surrounding whitespace, so strip before testing for emptiness {% endcomment %}
13
14
  {% capture start_date %}
14
15
  {% if entry.start_date %}{{ entry.start_date }}{% elsif entry.startDate %}{{ entry.startDate }}{% endif %}
15
16
  {% endcapture %}
17
+ {% assign start_date = start_date | strip %}
16
18
  {% capture end_date %}
17
19
  {% if entry.end_date %}{{ entry.end_date }}{% elsif entry.endDate %}{{ entry.endDate }}{% endif %}
18
20
  {% endcapture %}
21
+ {% assign end_date = end_date | strip %}
19
22
 
20
23
  {% if start_date != '' %}
21
24
  {% comment %} Extract year only from dates like "1933-01-01" or just use "1933" as is {% endcomment %}
@@ -23,19 +26,22 @@
23
26
  {% assign endDate = end_date | split: '-' | first | default: 'Present' %}
24
27
  {% assign date = startDate | append: ' - ' | append: endDate %}
25
28
  {% else %}
26
- {% assign date = '' %}
29
+ {% assign date = null %}
27
30
  {% endif %}
28
31
 
29
32
  <table class="table-cv">
30
33
  <tbody>
31
34
  <tr>
32
35
  <td>
33
- <span class="badge font-weight-bold danger-color-dark text-uppercase align-middle" style="min-width: 75px">{{ date }}</span>
36
+ {% if date %}
37
+ <span class="badge font-weight-bold danger-color-dark text-uppercase align-middle" style="min-width: 75px">{{ date }}</span>
38
+ {% endif %}
34
39
  </td>
35
40
  </tr>
36
41
  {% capture location %}
37
42
  {% if entry.location %}{{ entry.location }}{% endif %}
38
43
  {% endcapture %}
44
+ {% assign location = location | strip %}
39
45
  {% if location != '' %}
40
46
  <tr>
41
47
  <td>
@@ -122,7 +122,7 @@
122
122
  {% assign empty_array = '' | split: ',' %}
123
123
  {% assign exp = cv.sections.Experience | default: empty_array %}
124
124
  {% assign vol = cv.sections.Volunteer | default: empty_array %}
125
- {% assign combined_experience = exp | concat: vol %}
125
+ {% assign combined_experience = exp | concat: vol | al_cv_sort_by_date %}
126
126
  {% if combined_experience.size > 0 %}
127
127
  <a class="anchor" id="experience"></a>
128
128
  <div class="card mt-3 p-3">
@@ -149,7 +149,7 @@
149
149
  <h3 class="card-title font-weight-medium">{{ section_title }}</h3>
150
150
  <div>
151
151
  {% if section_title == 'Education' %}
152
- {% assign entries = section_entries %}
152
+ {% assign entries = section_entries | al_cv_sort_by_date %}
153
153
  {% include cv/education.liquid %}
154
154
  {% elsif section_title == 'Awards' or section_title == 'Honors and Awards' %}
155
155
  {% assign entries = section_entries %}
@@ -261,7 +261,7 @@
261
261
  {% assign empty_array = '' | split: ',' %}
262
262
  {% assign work = site.data.resume.work | default: empty_array %}
263
263
  {% assign vol = site.data.resume.volunteer | default: empty_array %}
264
- {% assign combined_experience = work | concat: vol %}
264
+ {% assign combined_experience = work | concat: vol | al_cv_sort_by_date %}
265
265
  {% if combined_experience.size > 0 %}
266
266
  <a class="anchor" id="experience"></a>
267
267
  <div class="card mt-3 p-3">
@@ -279,7 +279,7 @@
279
279
  <div class="card mt-3 p-3">
280
280
  <h3 class="card-title font-weight-medium">Education</h3>
281
281
  <div>
282
- {% assign entries = site.data.resume.education %}
282
+ {% assign entries = site.data.resume.education | al_cv_sort_by_date %}
283
283
  {% include cv/education.liquid %}
284
284
  </div>
285
285
  </div>
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: al_folio_cv
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - al-folio maintainers
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-02-16 00:00:00.000000000 Z
11
+ date: 2026-07-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: jekyll
@@ -111,6 +111,8 @@ files:
111
111
  - README.md
112
112
  - assets/css/al-folio-cv.css
113
113
  - lib/al_folio_cv.rb
114
+ - lib/al_folio_cv/date_sorting.rb
115
+ - lib/al_folio_cv/filters.rb
114
116
  - lib/al_folio_cv/version.rb
115
117
  - templates/cv/awards.liquid
116
118
  - templates/cv/certificates.liquid
@@ -145,7 +147,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
145
147
  - !ruby/object:Gem::Version
146
148
  version: '0'
147
149
  requirements: []
148
- rubygems_version: 3.4.10
150
+ rubygems_version: 3.5.22
149
151
  signing_key:
150
152
  specification_version: 4
151
153
  summary: CV rendering plugin for al-folio v1.x