gopromotional-packlist 1.0.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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +58 -0
- data/exe/gopromotional-packlist +5 -0
- data/lib/gopromotional/packlist/cli.rb +92 -0
- data/lib/gopromotional/packlist/version.rb +7 -0
- data/lib/gopromotional/packlist.rb +142 -0
- data/lib/gopromotional-packlist.rb +1 -0
- metadata +53 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 9ec908cd28c90f92b3afb8f031eb66bff55605d65aed959a00c971bd61912b08
|
|
4
|
+
data.tar.gz: 954362a4df57b747f43a824296491e6683f77fd22d4e05417b406b2cd0100f2d
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 1a1e0cdac8cba2c9c1a02e5689462574f51b50d2f7489ec7550649bb5730fc4226dc54b8b73e8b456a68956ac00ee8e152098d8ff8aa88fde668e436e8da9dc9
|
|
7
|
+
data.tar.gz: 78d9460426c688ee27539c57601adba3be1c99579ca097c4dc08f6e62a53f29c262192905337a594b9a52c3dc24c17b83f84a28a6dd6a03fcbde387857b969f7
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 GoPromotional
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# gopromotional-packlist
|
|
2
|
+
|
|
3
|
+
Build a packing checklist from templates (work day, travel, trade show, gym), scale clothes to the number of nights, add your own items, tick things off and see what is left. Lists are stored as plain Markdown checklists you can also edit by hand.
|
|
4
|
+
|
|
5
|
+
- **4 templates**: work day (10 items), travel (19), trade show (16), gym (9). Combine them; duplicates are merged.
|
|
6
|
+
- **Per-night quantities**: T-shirts, underwear and socks scale to nights + 1.
|
|
7
|
+
- **Tick off** items by a unique part of their name; see **progress** and what is **still to pack**.
|
|
8
|
+
- **Markdown storage**: `- [x] 3 × T-shirts` round-trips, so the list works in any editor, GitHub or Obsidian.
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
gem install gopromotional-packlist
|
|
14
|
+
# or in a Gemfile:
|
|
15
|
+
gem "gopromotional-packlist"
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Command line
|
|
19
|
+
|
|
20
|
+
```console
|
|
21
|
+
$ gopromotional-packlist templates
|
|
22
|
+
work-day (10 items)
|
|
23
|
+
travel (19 items)
|
|
24
|
+
trade-show (16 items)
|
|
25
|
+
gym (9 items)
|
|
26
|
+
|
|
27
|
+
$ gopromotional-packlist new --template trade-show --extra "Tote bags" --title Expo --out expo.md
|
|
28
|
+
Wrote 17 items to expo.md
|
|
29
|
+
|
|
30
|
+
$ gopromotional-packlist check expo.md gaffer
|
|
31
|
+
1/17 packed (6%)
|
|
32
|
+
|
|
33
|
+
$ gopromotional-packlist status expo.md
|
|
34
|
+
Expo: 1/17 packed (6%)
|
|
35
|
+
Still to pack: Pop-up banner / stand, Table cloth, Branded giveaways, Business cards, Brochures / flyers, Lead capture sheet or scanner, Extension lead, Laptop / tablet, Phone charger, Scissors, Cable ties, Pens, Water bottles, Snacks, First-aid kit, Tote bags
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Every command accepts `--json` for machine-readable output. Run `gopromotional-packlist --help` for all options.
|
|
39
|
+
|
|
40
|
+
## Library
|
|
41
|
+
|
|
42
|
+
```ruby
|
|
43
|
+
require "gopromotional/packlist"
|
|
44
|
+
P = GoPromotional::Packlist
|
|
45
|
+
|
|
46
|
+
items = P.build_list(templates: %w[travel work-day], nights: 3, extras: ["Umbrella"])
|
|
47
|
+
P.mark(items, "passport", true)
|
|
48
|
+
P.progress(items) # => {packed: 1, total: 28, percent: 4}
|
|
49
|
+
title, back = P.parse_markdown(P.to_markdown(items, "Lisbon"))
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Pure Ruby, no runtime dependencies, Ruby 3.0+. `require "gopromotional-packlist"` also works.
|
|
53
|
+
|
|
54
|
+
## Links
|
|
55
|
+
|
|
56
|
+
- Website: [branded tote bags](https://www.gopromotional.co.uk/branded-products/tote-bags)
|
|
57
|
+
- Source code: [https://github.com/nbkdevs/GoPromotional](https://github.com/nbkdevs/GoPromotional) (`packages/tote-bags/rubygems`)
|
|
58
|
+
- Licence: MIT
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "optparse"
|
|
5
|
+
require_relative "../packlist"
|
|
6
|
+
|
|
7
|
+
module GoPromotional
|
|
8
|
+
module Packlist
|
|
9
|
+
# Command line interface: templates | new | check | uncheck | add | status
|
|
10
|
+
module CLI
|
|
11
|
+
USAGE = <<~TXT
|
|
12
|
+
Usage: gopromotional-packlist <command> [options]
|
|
13
|
+
|
|
14
|
+
Commands:
|
|
15
|
+
templates list built-in templates
|
|
16
|
+
new [--template travel[,gym,...]] [--nights N] [--extra ITEM]... [--title T] [--out FILE]
|
|
17
|
+
check <file> <item> mark packed (unique partial names work)
|
|
18
|
+
uncheck <file> <item>
|
|
19
|
+
add <file> <item>
|
|
20
|
+
status <file> progress and what is still left
|
|
21
|
+
|
|
22
|
+
Options: --json, -v/--version, -h/--help
|
|
23
|
+
TXT
|
|
24
|
+
|
|
25
|
+
module_function
|
|
26
|
+
|
|
27
|
+
def run(argv, out: $stdout, err: $stderr)
|
|
28
|
+
o = { json: false, template: "travel", nights: 0, extras: [], title: "Packing list" }
|
|
29
|
+
parser = OptionParser.new do |p|
|
|
30
|
+
p.banner = USAGE
|
|
31
|
+
p.on("--json") { o[:json] = true }
|
|
32
|
+
p.on("--template NAMES") { |v| o[:template] = v }
|
|
33
|
+
p.on("--nights N", Integer) { |v| o[:nights] = v }
|
|
34
|
+
p.on("--extra ITEM") { |v| o[:extras] << v }
|
|
35
|
+
p.on("--title TITLE") { |v| o[:title] = v }
|
|
36
|
+
p.on("--out FILE") { |v| o[:out] = v }
|
|
37
|
+
p.on("-v", "--version") { out.puts VERSION; return 0 }
|
|
38
|
+
p.on("-h", "--help") { out.puts USAGE; return 0 }
|
|
39
|
+
end
|
|
40
|
+
args = parser.parse(argv)
|
|
41
|
+
cmd = args.shift
|
|
42
|
+
case cmd
|
|
43
|
+
when nil
|
|
44
|
+
out.puts USAGE
|
|
45
|
+
when "templates"
|
|
46
|
+
out.puts(o[:json] ? JSON.pretty_generate(TEMPLATES.transform_values { |v| v.map(&:to_h) }) : TEMPLATES.map { |k, v| "#{k} (#{v.size} items)" })
|
|
47
|
+
when "new"
|
|
48
|
+
names = o[:template].split(",").map(&:strip).reject(&:empty?)
|
|
49
|
+
items = Packlist.build_list(templates: names, nights: o[:nights], extras: o[:extras])
|
|
50
|
+
md = Packlist.to_markdown(items, o[:title])
|
|
51
|
+
if o[:out]
|
|
52
|
+
File.write(o[:out], md)
|
|
53
|
+
out.puts "Wrote #{items.size} items to #{o[:out]}"
|
|
54
|
+
else
|
|
55
|
+
out.puts(o[:json] ? JSON.pretty_generate(items.map(&:to_h)) : md)
|
|
56
|
+
end
|
|
57
|
+
when "check", "uncheck", "add"
|
|
58
|
+
file = args.shift
|
|
59
|
+
name = args.join(" ")
|
|
60
|
+
raise ArgumentError, "#{cmd} needs a file and an item name" if file.nil? || name.empty?
|
|
61
|
+
title, items = Packlist.parse_markdown(File.read(file, encoding: "utf-8"))
|
|
62
|
+
if cmd == "add"
|
|
63
|
+
raise ArgumentError, %("#{name}" is already on the list) if items.any? { |i| i.name.downcase == name.downcase }
|
|
64
|
+
items << Item.new(name, "Extras")
|
|
65
|
+
else
|
|
66
|
+
Packlist.mark(items, name, cmd == "check")
|
|
67
|
+
end
|
|
68
|
+
File.write(file, Packlist.to_markdown(items, title))
|
|
69
|
+
pr = Packlist.progress(items)
|
|
70
|
+
out.puts "#{pr[:packed]}/#{pr[:total]} packed (#{pr[:percent]}%)"
|
|
71
|
+
when "status"
|
|
72
|
+
file = args.shift or raise ArgumentError, "status needs a file"
|
|
73
|
+
title, items = Packlist.parse_markdown(File.read(file, encoding: "utf-8"))
|
|
74
|
+
pr = Packlist.progress(items)
|
|
75
|
+
left = Packlist.remaining(items)
|
|
76
|
+
if o[:json]
|
|
77
|
+
out.puts JSON.pretty_generate(pr.merge(remaining: left.map(&:to_h)))
|
|
78
|
+
else
|
|
79
|
+
out.puts "#{title}: #{pr[:packed]}/#{pr[:total]} packed (#{pr[:percent]}%)"
|
|
80
|
+
out.puts(left.empty? ? "All packed." : "Still to pack: #{left.map(&:name).join(', ')}")
|
|
81
|
+
end
|
|
82
|
+
else
|
|
83
|
+
raise ArgumentError, %(Unknown command "#{cmd}". Run with --help.)
|
|
84
|
+
end
|
|
85
|
+
0
|
|
86
|
+
rescue ArgumentError, OptionParser::ParseError, SystemCallError => e
|
|
87
|
+
err.puts "Error: #{e.message}"
|
|
88
|
+
2
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "packlist/version"
|
|
4
|
+
|
|
5
|
+
module GoPromotional
|
|
6
|
+
# Packing checklists with templates, per-night quantities and a round-trip Markdown format.
|
|
7
|
+
module Packlist
|
|
8
|
+
TemplateItem = Struct.new(:name, :category, :per_night) do
|
|
9
|
+
def to_h = { name: name, category: category, per_night: per_night }
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def self.t(rows) = rows.map { |n, c, p| TemplateItem.new(n, c, p || false).freeze }.freeze
|
|
13
|
+
|
|
14
|
+
TEMPLATES = {
|
|
15
|
+
"work-day" => t([
|
|
16
|
+
["Laptop", "Work"], ["Laptop charger", "Work"], ["Phone charger", "Work"], ["Badge / ID", "Work"],
|
|
17
|
+
["Notebook", "Work"], ["Pen", "Work"], ["Headphones", "Work"], ["Water bottle", "Work"],
|
|
18
|
+
["Reusable coffee cup", "Work"], ["Lunch / snacks", "Work"]
|
|
19
|
+
]),
|
|
20
|
+
"travel" => t([
|
|
21
|
+
["Passport / ID", "Documents"], ["Tickets / boarding pass", "Documents"], ["Bank cards", "Documents"],
|
|
22
|
+
["T-shirts", "Clothes", true], ["Underwear", "Clothes", true], ["Socks", "Clothes", true],
|
|
23
|
+
["Trousers / skirt", "Clothes"], ["Jumper", "Clothes"], ["Sleepwear", "Clothes"],
|
|
24
|
+
["Toothbrush", "Toiletries"], ["Toothpaste", "Toiletries"], ["Deodorant", "Toiletries"], ["Medications", "Toiletries"],
|
|
25
|
+
["Phone charger", "Tech"], ["Power bank", "Tech"], ["Plug adapter", "Tech"], ["Headphones", "Tech"],
|
|
26
|
+
["Reusable water bottle", "Other"], ["Tote bag", "Other"]
|
|
27
|
+
]),
|
|
28
|
+
"trade-show" => t([
|
|
29
|
+
["Pop-up banner / stand", "Stand"], ["Table cloth", "Stand"], ["Branded giveaways", "Stand"],
|
|
30
|
+
["Business cards", "Stand"], ["Brochures / flyers", "Stand"], ["Lead capture sheet or scanner", "Stand"],
|
|
31
|
+
["Extension lead", "Kit"], ["Laptop / tablet", "Kit"], ["Phone charger", "Kit"], ["Gaffer tape", "Kit"],
|
|
32
|
+
["Scissors", "Kit"], ["Cable ties", "Kit"], ["Pens", "Kit"],
|
|
33
|
+
["Water bottles", "Team"], ["Snacks", "Team"], ["First-aid kit", "Team"]
|
|
34
|
+
]),
|
|
35
|
+
"gym" => t([
|
|
36
|
+
["Gym clothes", "Gym"], ["Trainers", "Gym"], ["Towel", "Gym"], ["Water bottle", "Gym"], ["Padlock", "Gym"],
|
|
37
|
+
["Headphones", "Gym"], ["Shower gel", "Gym"], ["Deodorant", "Gym"], ["Snack", "Gym"]
|
|
38
|
+
])
|
|
39
|
+
}.freeze
|
|
40
|
+
|
|
41
|
+
Item = Struct.new(:name, :category, :qty, :packed) do
|
|
42
|
+
def initialize(name, category, qty = 1, packed = false) = super
|
|
43
|
+
def label = qty > 1 ? "#{qty} × #{name}" : name
|
|
44
|
+
def to_h = { name: name, category: category, qty: qty, packed: packed }
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
module_function
|
|
48
|
+
|
|
49
|
+
def key(s) = s.to_s.strip.downcase
|
|
50
|
+
|
|
51
|
+
# Build a checklist. Per-night items get nights + 1 (one spare); duplicate names are merged (first wins).
|
|
52
|
+
def build_list(templates: ["travel"], nights: 0, extras: [])
|
|
53
|
+
raise ArgumentError, "nights must be an integer from 0 to 365" unless nights.is_a?(Integer) && (0..365).cover?(nights)
|
|
54
|
+
seen = {}
|
|
55
|
+
items = []
|
|
56
|
+
push = lambda do |it|
|
|
57
|
+
k = key(it.name)
|
|
58
|
+
next if k.empty? || seen[k]
|
|
59
|
+
seen[k] = true
|
|
60
|
+
items << it
|
|
61
|
+
end
|
|
62
|
+
templates.each do |t|
|
|
63
|
+
tpl = TEMPLATES[t] or raise ArgumentError, %(Unknown template "#{t}". Choose from: #{TEMPLATES.keys.join(', ')})
|
|
64
|
+
tpl.each { |r| push.call(Item.new(r.name, r.category, r.per_night && nights.positive? ? nights + 1 : 1)) }
|
|
65
|
+
end
|
|
66
|
+
extras.each { |e| push.call(Item.new(e.to_s.strip, "Extras")) }
|
|
67
|
+
items
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def find(items, name)
|
|
71
|
+
k = key(name)
|
|
72
|
+
exact = items.find { |i| key(i.name) == k }
|
|
73
|
+
return exact if exact
|
|
74
|
+
partial = items.select { |i| key(i.name).include?(k) }
|
|
75
|
+
return partial.first if partial.size == 1
|
|
76
|
+
raise ArgumentError, %("#{name}" matches #{partial.size} items: #{partial.map(&:name).join(', ')}) if partial.any?
|
|
77
|
+
raise ArgumentError, %(No item matches "#{name}")
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Mark an item packed (toggle when packed is nil). Exact name first, then a unique substring.
|
|
81
|
+
def mark(items, name, packed = nil)
|
|
82
|
+
it = find(items, name)
|
|
83
|
+
it.packed = packed.nil? ? !it.packed : packed ? true : false
|
|
84
|
+
it
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def progress(items)
|
|
88
|
+
packed = items.count(&:packed)
|
|
89
|
+
{ packed: packed, total: items.size, percent: items.empty? ? 0 : (packed * 100.0 / items.size + 0.5).floor }
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def remaining(items) = items.reject(&:packed)
|
|
93
|
+
|
|
94
|
+
def filter_items(items, query)
|
|
95
|
+
q = key(query)
|
|
96
|
+
items.select { |i| key(i.name).include?(q) || key(i.category).include?(q) }
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def categories(items) = items.map(&:category).uniq
|
|
100
|
+
|
|
101
|
+
# Markdown checkboxes grouped by category; parse_markdown reads it back.
|
|
102
|
+
def to_markdown(items, title = "Packing list")
|
|
103
|
+
lines = ["# #{title}", ""]
|
|
104
|
+
categories(items).each do |c|
|
|
105
|
+
lines << "## #{c}"
|
|
106
|
+
items.select { |i| i.category == c }.each { |i| lines << "- [#{i.packed ? 'x' : ' '}] #{i.label}" }
|
|
107
|
+
lines << ""
|
|
108
|
+
end
|
|
109
|
+
lines.join("\n")
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def to_text(items)
|
|
113
|
+
categories(items).map do |c|
|
|
114
|
+
([c] + items.select { |i| i.category == c }.map { |i| " #{i.packed ? '[x]' : '[ ]'} #{i.label}" }).join("\n")
|
|
115
|
+
end.join("\n")
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Returns [title, items].
|
|
119
|
+
def parse_markdown(md)
|
|
120
|
+
title = "Packing list"
|
|
121
|
+
category = "Items"
|
|
122
|
+
items = []
|
|
123
|
+
md.to_s.each_line do |raw|
|
|
124
|
+
line = raw.strip
|
|
125
|
+
if (m = line.match(/\A#\s+(.+)\z/))
|
|
126
|
+
title = m[1].strip
|
|
127
|
+
elsif (m = line.match(/\A##\s+(.+)\z/))
|
|
128
|
+
category = m[1].strip
|
|
129
|
+
elsif (m = line.match(/\A[-*]\s+\[([ xX])\]\s+(.+)\z/))
|
|
130
|
+
name = m[2].strip
|
|
131
|
+
qty = 1
|
|
132
|
+
if (q = name.match(/\A(\d+)\s*[×x]\s+(.+)\z/))
|
|
133
|
+
qty = q[1].to_i
|
|
134
|
+
name = q[2].strip
|
|
135
|
+
end
|
|
136
|
+
items << Item.new(name, category, qty, m[1] != " ")
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
[title, items]
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
require_relative "gopromotional/packlist"
|
metadata
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: gopromotional-packlist
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- GoPromotional
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies: []
|
|
12
|
+
description: Build a packing checklist from templates (work day, travel, trade show,
|
|
13
|
+
gym), scale clothes to the number of nights, add your own items, tick things off
|
|
14
|
+
and see what is left. Lists are stored as plain Markdown checklists you can also
|
|
15
|
+
edit by hand.
|
|
16
|
+
executables:
|
|
17
|
+
- gopromotional-packlist
|
|
18
|
+
extensions: []
|
|
19
|
+
extra_rdoc_files: []
|
|
20
|
+
files:
|
|
21
|
+
- LICENSE
|
|
22
|
+
- README.md
|
|
23
|
+
- exe/gopromotional-packlist
|
|
24
|
+
- lib/gopromotional-packlist.rb
|
|
25
|
+
- lib/gopromotional/packlist.rb
|
|
26
|
+
- lib/gopromotional/packlist/cli.rb
|
|
27
|
+
- lib/gopromotional/packlist/version.rb
|
|
28
|
+
homepage: https://www.gopromotional.co.uk/branded-products/tote-bags
|
|
29
|
+
licenses:
|
|
30
|
+
- MIT
|
|
31
|
+
metadata:
|
|
32
|
+
homepage_uri: https://www.gopromotional.co.uk/branded-products/tote-bags
|
|
33
|
+
source_code_uri: https://github.com/nbkdevs/GoPromotional/tree/main/packages/tote-bags/rubygems
|
|
34
|
+
bug_tracker_uri: https://github.com/nbkdevs/GoPromotional/issues
|
|
35
|
+
rubygems_mfa_required: 'true'
|
|
36
|
+
rdoc_options: []
|
|
37
|
+
require_paths:
|
|
38
|
+
- lib
|
|
39
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
40
|
+
requirements:
|
|
41
|
+
- - ">="
|
|
42
|
+
- !ruby/object:Gem::Version
|
|
43
|
+
version: '3.0'
|
|
44
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
45
|
+
requirements:
|
|
46
|
+
- - ">="
|
|
47
|
+
- !ruby/object:Gem::Version
|
|
48
|
+
version: '0'
|
|
49
|
+
requirements: []
|
|
50
|
+
rubygems_version: 3.6.7
|
|
51
|
+
specification_version: 4
|
|
52
|
+
summary: Packing checklists with templates, per-night quantities and a Markdown format.
|
|
53
|
+
test_files: []
|