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 +4 -4
- data/CHANGELOG.md +5 -0
- data/README.md +13 -6
- data/lib/al_folio_cv/date_sorting.rb +142 -0
- data/lib/al_folio_cv/filters.rb +14 -0
- data/lib/al_folio_cv/version.rb +1 -1
- data/lib/al_folio_cv.rb +2 -0
- data/templates/cv/education.liquid +4 -0
- data/templates/cv/experience.liquid +8 -2
- data/templates/cv/render.liquid +4 -4
- metadata +5 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 735f4f1219fb50c22009f42027dd424035eff20e82146b91f0f94670a3df1951
|
|
4
|
+
data.tar.gz: '0428b2b70e9be220422a77c22f6fa84f1b84e0bbdc1bf0e01628f03ab26aa948'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
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
|
-
##
|
|
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
|
-
```
|
|
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
|
data/lib/al_folio_cv/version.rb
CHANGED
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
|
-
|
|
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>
|
data/templates/cv/render.liquid
CHANGED
|
@@ -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.
|
|
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-
|
|
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.
|
|
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
|