labrat 0.1.13

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.
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Labrat
4
+ class OptionError < StandardError; end
5
+
6
+ class DimensionError < StandardError; end
7
+
8
+ class LabelNameError < StandardError; end
9
+
10
+ class EmptyLabelError < StandardError; end
11
+
12
+ class RecursionError < StandardError; end
13
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+ class Hash
3
+ # Transform hash keys to symbols suitable for calling as methods, i.e.,
4
+ # translate any hyphens to underscores. This is the form we want to keep
5
+ # config hashes in Labrat.
6
+ def methodize
7
+ transform_keys { |k| k.to_s.gsub('-', '_').to_sym }
8
+ end
9
+
10
+ # Convert the given Hash into a Array of Strings that represent an
11
+ # equivalent set of command-line args and pass them into the #parse method.
12
+ def optionize
13
+ options = []
14
+ each_pair do |k, v|
15
+ key = k.to_s.gsub('_', '-')
16
+ options <<
17
+ if [TrueClass, FalseClass].include?(v.class)
18
+ v ? "--#{key}" : "--no-#{key}"
19
+ else
20
+ "--#{key}=#{v}"
21
+ end
22
+ end
23
+ options
24
+ end
25
+
26
+ def report(title)
27
+ warn "#{title}:"
28
+ if empty?
29
+ warn " [[Empty]]"
30
+ else
31
+ each do |k, v|
32
+ val = v.class == Float ? v.round(2).to_s + 'pt' : v
33
+ warn " #{k}: #{val}"
34
+ end
35
+ end
36
+ warn ""
37
+ end
38
+ end
@@ -0,0 +1,158 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Labrat
4
+ class Label
5
+ attr_reader :ops
6
+ attr_accessor :texts
7
+
8
+ def initialize(texts, ops)
9
+ @ops = ops
10
+ unless @ops.nl_sep.nil? || @ops.nl_sep == ''
11
+ @texts = texts.map { |t| t.gsub(ops.nl_sep, "\n") }
12
+ end
13
+ if @ops.copies > 1
14
+ duped_texts = []
15
+ @texts.each { |t| @ops.copies.times { duped_texts << t } }
16
+ @texts = duped_texts
17
+ end
18
+ end
19
+
20
+ def generate
21
+ # The default margin is 0.5in on all sides, way too big for labels, so
22
+ # it is important to set these here. The margins' designation as "top,"
23
+ # "left," "bottom," and "right" take into account the page layout. That
24
+ # is, the left margin is on the left in both portrait and landscape
25
+ # orientations. But I want the user to be able to set the margins
26
+ # according to the label type, independent of the orientation. I adopt
27
+ # the convention that the margins are named assuming a portrait
28
+ # orientation and swap them here so that when Prawn swaps them again,
29
+ # they come out correct.
30
+ layout = ops.landscape ? :landscape : :portrait
31
+ if layout == :portrait
32
+ tpm = ops.top_page_margin
33
+ bpm = ops.bottom_page_margin
34
+ lpm = ops.left_page_margin
35
+ rpm = ops.right_page_margin
36
+ else
37
+ lpm = ops.top_page_margin
38
+ rpm = ops.bottom_page_margin
39
+ tpm = ops.left_page_margin
40
+ bpm = ops.right_page_margin
41
+ end
42
+ out_file = File.expand_path(ops.out_file)
43
+ Prawn::Document.generate(out_file, page_size: [ops.page_width, ops.page_height],
44
+ left_margin: lpm, right_margin: rpm,
45
+ top_margin: tpm, bottom_margin: bpm,
46
+ page_layout: layout) do |pdf|
47
+ # Define a grid with each grid box to be used for a single label.
48
+ pdf.define_grid(rows: ops.rows, columns: ops.columns,
49
+ row_gutter: ops.row_gap, column_gutter: ops.column_gap)
50
+ if ops.verbose
51
+ warn "Page dimensions:"
52
+ warn " [pg_wd, pg_ht] = [#{ops.page_width.round(2)}pt,#{ops.page_height.round(2)}pt]"
53
+ warn " orientation: #{layout}"
54
+ warn " [rows, columns] = [#{ops.rows},#{ops.columns}]"
55
+ warn " [lpm, rpm] = [#{lpm.round(2)}pt,#{rpm.round(2)}pt]"
56
+ warn " [tpm, bpm] = [#{tpm.round(2)}pt,#{bpm.round(2)}pt]"
57
+ warn ""
58
+ end
59
+ if ops.template
60
+ # Replace any texts with the numbers and show the grid.
61
+ self.texts = (1..(ops.rows * ops.columns)).map(&:to_s)
62
+ ops.font_name = 'Helvetica'
63
+ ops.font_style = 'bold'
64
+ ops.font_size = 16
65
+ pdf.grid.show_all
66
+ end
67
+ raise EmptyLabelError, "Empty label" if waste_of_labels?
68
+
69
+ last_k = texts.size - 1
70
+ lab_dims_reported = false
71
+ texts.each_with_index do |text, k|
72
+ row, col = row_col(k + 1)
73
+ pdf.grid(row, col).bounding_box do
74
+ bounds = pdf.bounds
75
+ pdf.stroke_bounds if ops.grid
76
+ box_wd = (bounds.right - bounds.left) - ops.left_pad - ops.right_pad
77
+ box_ht = (bounds.top - bounds.bottom) - ops.top_pad - ops.bottom_pad
78
+ box_x = ops.left_pad + ops.delta_x
79
+ box_y = ops.bottom_pad + box_ht + ops.delta_y
80
+ pdf.font ops.font_name, style: ops.font_style, size: ops.font_size.to_f
81
+ pdf.text_box(text, width: box_wd, height: box_ht,
82
+ align: ops.h_align, valign: ops.v_align,
83
+ overflow: :truncate, at: [box_x, box_y])
84
+ if ops.verbose && !lab_dims_reported
85
+ warn "Label text box dimensions:"
86
+ warn " [box_wd, box_ht] = [#{box_wd.round(2)}pt,#{box_ht.round(2)}pt]"
87
+ warn " [box_x, box_y] = [#{box_x.round(2)}pt,#{box_y.round(2)}pt]"
88
+ warn " [delta_x, delta_y] = [#{ops.delta_x.round(2)}pt,#{ops.delta_y.round(2)}pt]"
89
+ warn ''
90
+ lab_dims_reported = true
91
+ end
92
+ if ops.verbose
93
+ warn "Label \##{(k % lpp) + 1} on page #{page_num(k)} at row #{row + 1}, column #{col + 1}:"
94
+ warn '-------------------'
95
+ warn text
96
+ warn '-------------------'
97
+ warn ''
98
+ end
99
+ pdf.start_new_page if needs_new_page?(k, last_k)
100
+ end
101
+ end
102
+ end
103
+ self
104
+ end
105
+
106
+ def print
107
+ cmd = ops.print_command.gsub('%p', ops.printer).gsub('%o', ops.out_file)
108
+ if ops.verbose
109
+ warn "Printing with:"
110
+ warn " #{cmd} &"
111
+ end
112
+ system("#{cmd} &")
113
+ end
114
+
115
+ def view
116
+ cmd = ops.view_command.gsub('%o', ops.out_file)
117
+ if ops.verbose
118
+ warn "Viewing with:"
119
+ warn " #{cmd} &"
120
+ end
121
+ system("#{cmd} &")
122
+ end
123
+
124
+ def remove
125
+ FileUtils.rm(ops.out_file)
126
+ end
127
+
128
+ # Labels per page
129
+ def lpp
130
+ ops.rows * ops.columns
131
+ end
132
+
133
+ # Page number of the kth label
134
+ def page_num(k)
135
+ k.divmod(lpp)[0] + 1
136
+ end
137
+
138
+ # Return the 0-based row and column within a page on which the k-th
139
+ # (1-based) label should be printed.
140
+ def row_col(k)
141
+ k_on_page = (k + ops.start_label - 2) % lpp
142
+ k_on_page.divmod(ops.columns)
143
+ end
144
+
145
+ # Should we emit a new page at this point?
146
+ def needs_new_page?(k, last_k)
147
+ return false if k == last_k
148
+ r, c = row_col(k + 1)
149
+ (r + 1 == ops.rows && c + 1 == ops.columns)
150
+ end
151
+
152
+ # Would we just be printing blank labels?
153
+ def waste_of_labels?
154
+ (texts.nil? || texts.empty? || texts.all?(&:blank?)) &&
155
+ !ops.view
156
+ end
157
+ end
158
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Labrat
4
+ module LabelDb
5
+ class << self
6
+ # Module-level variable to hold the merged database.
7
+ attr_accessor :db
8
+ end
9
+
10
+ # Read in the Labrat database of label settings, merging system and user
11
+ # databases.
12
+ def self.read(dir_prefix: '')
13
+ self.db = Config.read('labrat', base: 'labeldb', dir_prefix: dir_prefix)
14
+ .transform_keys(&:to_sym)
15
+ end
16
+
17
+ # Return a hash of config settings for the label named by labname.
18
+ def self.[](labname)
19
+ read unless db
20
+ db[labname.to_sym] || {}
21
+ end
22
+
23
+ # Set a runtime configuration for a single labelname.
24
+ def self.[]=(labname, config = {})
25
+ read unless db
26
+ db[labname.to_sym] = config
27
+ end
28
+
29
+ # Return an Array of label names.
30
+ def self.known_names
31
+ read unless db
32
+ db.keys.sort
33
+ end
34
+
35
+ def self.db_paths(dir_prefix = '')
36
+ system_db_paths(dir_prefix) + user_db_paths(dir_prefix)
37
+ end
38
+
39
+ def self.system_db_paths(dir_prefix = '')
40
+ paths = Config.config_paths('labrat', base: 'labeldb', dir_prefix: dir_prefix)
41
+ paths[:system]
42
+ end
43
+
44
+ def self.user_db_paths(dir_prefix = '')
45
+ paths = Config.config_paths('labrat', base: 'labeldb', dir_prefix: dir_prefix)
46
+ paths[:user]
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,184 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Labrat
4
+ # The Options class is a glorified Hash, a container for the options
5
+ # settings gathered from the defaults, the config files, the command line,
6
+ # and perhaps environment. An Options instance can be handed off to the
7
+ # label-printing objects to inform its formatting, printing, etc.
8
+ class Options
9
+ attr_accessor :label, :page_width, :page_height,
10
+ :left_page_margin, :right_page_margin,
11
+ :top_page_margin, :bottom_page_margin,
12
+ :rows, :columns, :row_gap, :column_gap, :landscape,
13
+ :start_label, :grid,
14
+ :h_align, :v_align,
15
+ :left_pad, :right_pad, :top_pad, :bottom_pad,
16
+ :delta_x, :delta_y,
17
+ :font_name, :font_style, :font_size,
18
+ :in_file, :nl_sep, :label_sep, :copies,
19
+ :printer, :out_file, :print_command, :view_command, :view,
20
+ :template, :verbose, :msg
21
+
22
+ # Initialize with an optional hash of default values for the attributes.
23
+ def initialize(**init)
24
+ self.label = init[:label] || nil
25
+ # Per-page attributes
26
+ self.page_width = init[:page_width] || 24.mm
27
+ self.page_height = init[:page_height] || 87.mm
28
+ self.left_page_margin = init[:left_page_margin] || 5.mm
29
+ self.right_page_margin = init[:right_page_margin] || 5.mm
30
+ self.top_page_margin = init[:top_page_margin] || 0.mm
31
+ self.bottom_page_margin = init[:bottom_page_margin] || 0.mm
32
+ self.rows = init[:rows] || 1
33
+ self.columns = init[:columns] || 1
34
+ self.row_gap = init[:row_gap] || 0.mm
35
+ self.column_gap = init[:column_gap] || 0.mm
36
+ self.start_label = init[:start_label] || 1
37
+ self.landscape = init.fetch(:landscape, false)
38
+ # Per-label attributes
39
+ self.h_align = init[:h_align]&.to_sym || :center
40
+ self.v_align = init[:v_align]&.to_sym || :center
41
+ self.left_pad = init[:left_pad] || 4.5.mm
42
+ self.right_pad = init[:right_pad] || 4.5.mm
43
+ self.top_pad = init[:top_pad] || 0
44
+ self.bottom_pad = init[:bottom_pad] || 0
45
+ self.delta_x = init[:delta_x] || 0
46
+ self.delta_y = init[:delta_y] || 0
47
+ self.font_name = init[:font_name] || 'Helvetica'
48
+ self.font_style = init[:font_style]&.to_sym || :normal
49
+ self.font_size = init[:font_size] || 12
50
+ # Input attributes
51
+ self.in_file = init[:in_file] || nil
52
+ self.nl_sep = init[:nl_sep] || '++'
53
+ self.label_sep = init[:label_sep] || ']*['
54
+ self.copies = init[:copies] || 1
55
+ # Output attributes
56
+ self.printer = init[:printer] || ENV['LABRAT_PRINTER'] || ENV['PRINTER'] || 'dymo'
57
+ self.out_file = init[:out_file] || 'labrat.pdf'
58
+ self.print_command = init[:print_command] || 'lpr -P %p %o'
59
+ self.view_command = init[:view_command] || 'qpdfview --unique --instance labrat %o'
60
+ self.view = init.fetch(:view, false)
61
+ self.template = init.fetch(:landscape, false)
62
+ self.grid = init.fetch(:gid, false)
63
+ self.verbose = init.fetch(:verbose, false)
64
+ self.msg = init[:msg] || nil
65
+ end
66
+
67
+ # High-level setting of options from config files, and given command-line
68
+ # args.
69
+ def self.set_options(args, verbose: false)
70
+ # Default, built-in config; set verbose to param.
71
+ default_config = Labrat::Options.new(verbose: verbose).to_hash
72
+ default_config.report("Default settings") if verbose
73
+
74
+ # Config files
75
+ file_config = Labrat::Config.read('labrat', verbose: verbose)
76
+ file_config.report("Settings from merged config files") if verbose
77
+ file_options = Labrat::ArgParser.new.parse(file_config, prior: default_config, verbose: verbose)
78
+
79
+ # Command-line
80
+ if verbose
81
+ warn "Command-line:"
82
+ args.each do |arg|
83
+ warn arg.to_s
84
+ end
85
+ warn ""
86
+ end
87
+ Labrat::ArgParser.new.parse(args, prior: file_options, verbose: verbose)
88
+ end
89
+
90
+ # Return any string in msg, e.g., the usage help or error.
91
+ def to_s
92
+ msg
93
+ end
94
+
95
+ # Allow hash-like assignment to attributes. This allows an Options object
96
+ # to be used, for example, in the OptionParser#parse :into parameter.
97
+ def []=(att, val)
98
+ att = att.to_s.gsub('-', '_')
99
+ send("#{att}=", val)
100
+ end
101
+
102
+ # Allow hash-like access to attributes. This allows an Options object
103
+ # to be used, for example, in the OptionParser#parse :into parameter.
104
+ def [](att)
105
+ att = att.to_s.gsub('-', '_')
106
+ send(att.to_s)
107
+ end
108
+
109
+ # For testing, return an Array of the attributes as symbols.
110
+ def self.attrs
111
+ instance_methods(false).grep(/\A[a-z_]+=\Z/)
112
+ .map { |a| a.to_s.sub(/=\z/, '').to_sym }
113
+ end
114
+
115
+ # For testing, return an Array of the flags-form of the attributes, i.e.,
116
+ # with the underscores, _, replaced with hyphens.
117
+ def self.flags
118
+ attrs.map { |a| a.gsub('_', '-') }
119
+ end
120
+
121
+ # Return a hash of the values in this Options object. This is the
122
+ # canonical form of a Hash for Labrat, i.e., symbolic keys with any
123
+ # hyphens translated into underscores. Don't include the msg attribute.
124
+ def to_hash
125
+ {
126
+ label: label,
127
+ page_width: page_width,
128
+ page_height: page_height,
129
+ left_page_margin: left_page_margin,
130
+ right_page_margin: right_page_margin,
131
+ top_page_margin: top_page_margin,
132
+ bottom_page_margin: bottom_page_margin,
133
+ rows: rows,
134
+ columns: columns,
135
+ row_gap: row_gap,
136
+ column_gap: column_gap,
137
+ grid: grid,
138
+ start_label: start_label,
139
+ landscape: landscape,
140
+ # Per-label attributes
141
+ h_align: h_align,
142
+ v_align: v_align,
143
+ left_pad: left_pad,
144
+ right_pad: right_pad,
145
+ top_pad: top_pad,
146
+ bottom_pad: bottom_pad,
147
+ delta_x: delta_x,
148
+ delta_y: delta_y,
149
+ font_name: font_name,
150
+ font_style: font_style,
151
+ font_size: font_size,
152
+ # Input attributes
153
+ in_file: in_file,
154
+ nl_sep: nl_sep,
155
+ label_sep: label_sep,
156
+ copies: copies,
157
+ # Output attributes
158
+ printer: printer,
159
+ out_file: out_file,
160
+ print_command: print_command,
161
+ view_command: view_command,
162
+ view: view,
163
+ template: template,
164
+ verbose: verbose,
165
+ }
166
+ end
167
+
168
+ # Update the fields of this Option instance by merging in the values in
169
+ # hsh into self. Ignore any keys in hsh not corresponding to a setter for
170
+ # an Options object.
171
+ def merge!(hsh)
172
+ # Convert any separator hyphens in the hash keys to underscores
173
+ hsh = hsh.to_hash.transform_keys { |key| key.to_s.gsub('-', '_').to_sym }
174
+ new_hash = to_hash.merge(hsh)
175
+ new_hash.each_pair do |k, val|
176
+ setter = "#{k}=".to_sym
177
+ next unless respond_to?(setter)
178
+
179
+ send(setter, val)
180
+ end
181
+ self
182
+ end
183
+ end
184
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Labrat
4
+ def self.read_label_texts(fname, nlsep)
5
+ file =
6
+ if fname
7
+ ofname = fname
8
+ fname = File.expand_path(fname)
9
+ unless File.readable?(fname)
10
+ raise "Cannot open label file '#{ofname}' for reading"
11
+ end
12
+ File.open(fname)
13
+ else
14
+ $stdin
15
+ end
16
+
17
+ texts = []
18
+ label = nil
19
+ file.each do |line|
20
+ next if line =~ /\A#/
21
+
22
+ if line =~ /\A\s*\z/
23
+ # At blank line record any accumulated label into texts, but remove
24
+ # the nlsep from the end.
25
+ if label
26
+ texts << label.sub(/#{Regexp.quote(nlsep)}\z/, '')
27
+ label = nil
28
+ end
29
+ else
30
+ # Append a non-blank line to the current label, creating it if
31
+ # necessary.
32
+ label ||= +''
33
+ label << line.chomp + nlsep
34
+ end
35
+ end
36
+ # Last label in the file.
37
+ texts << label.sub(/#{Regexp.quote(nlsep)}\z/, '') if label
38
+ texts
39
+ end
40
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Labrat
4
+ VERSION = "0.1.13"
5
+ end
data/lib/labrat.rb ADDED
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support'
4
+ require 'active_support/core_ext'
5
+ require 'fat_core/enumerable'
6
+ require 'prawn'
7
+ require 'prawn/measurement_extensions'
8
+ require 'optparse'
9
+ require 'yaml'
10
+ require 'pp'
11
+
12
+ require_relative "labrat/version"
13
+ require_relative "labrat/errors"
14
+ require_relative "labrat/hash"
15
+ require_relative "labrat/options"
16
+ require_relative "labrat/arg_parser"
17
+ require_relative "labrat/label"
18
+ require_relative "labrat/config"
19
+ require_relative "labrat/label_db"
20
+ require_relative "labrat/read_files"
@@ -0,0 +1,108 @@
1
+ ;;;; labrat -- Print labels using labrat from within a buffer.
2
+
3
+ ;;; Commentary:
4
+
5
+ ;; Lisp commands to print labels from within Emacs a buffer by invoking
6
+ ;; labrat, a Ruby application desgined for printing labels from the
7
+ ;; command-line rather than through a GUI. You must have labrat installed
8
+ ;; first. See https://github.com/ddoherty03/labrat for details.
9
+
10
+ ;;; Code:
11
+
12
+ (require 'thingatpt)
13
+ (require 's)
14
+ (require 'dash)
15
+
16
+ (defcustom labrat-executable "labrat"
17
+ "Executable for labrat.
18
+
19
+ If the executable is not in your variable `exec-path', set this
20
+ to the full path name of the executable,
21
+ e.g. ~/.rbenv/shims/labrat, for an rbenv ruby installation."
22
+ :type 'string
23
+ :group 'labrat)
24
+
25
+ (defcustom labrat-nl-sep "++"
26
+ "String to mark newlines in label text.
27
+
28
+ If you change this, you need to make a corresponding change in your
29
+ labrat configuration at ~/.config/labrat/config.yml."
30
+ :type 'string
31
+ :group 'labrat)
32
+
33
+ (defcustom labrat-label-sep "]*["
34
+ "String to mark the separation between labels on the labrat command-line.
35
+
36
+ If you change this, you need to make a corresponding change in your
37
+ labrat configuration at ~/.config/labrat/config.yml."
38
+ :type 'string
39
+ :group 'labrat)
40
+
41
+ (defun labrat/pars-in-region ()
42
+ "Return a string of paragraphs in region, separated by `labrat-label-sep'.
43
+
44
+ If the region is not active, just return the paragraph at or before point as
45
+ is done by `labrat-par-at-point'. In either case strip comment lines."
46
+ (if (region-active-p)
47
+ (progn
48
+ (let ((beg (region-beginning))
49
+ (end (save-excursion
50
+ (progn (goto-char (region-end))
51
+ (forward-paragraph)
52
+ (point))))
53
+ (pars ""))
54
+ (progn
55
+ (save-excursion
56
+ (goto-char beg)
57
+ (while (< (point) end)
58
+ (forward-paragraph)
59
+ (setq pars (s-concat pars (labrat/par-at-point) labrat-label-sep))))
60
+ (s-chop-prefix labrat-label-sep (s-chop-suffix labrat-label-sep pars)))))
61
+ (labrat/par-at-point)))
62
+
63
+ (defun labrat/par-at-point ()
64
+ "Return the paragraph at or before point.
65
+
66
+ Similar to the command `thing-at-point' for paragraph, but look
67
+ for preceding paragraph even if there are several blank lines
68
+ before point, trim white space, comments, and properties from the
69
+ result."
70
+ (save-excursion
71
+ (unless (looking-at ".+")
72
+ (re-search-backward "^.+$" nil 'to-bob))
73
+ (s-replace "\n" labrat-nl-sep
74
+ (labrat/remove-comments
75
+ (s-trim (thing-at-point 'paragraph t))))))
76
+
77
+ (defun labrat/remove-comments (str)
78
+ "Remove any lines from STR that start with the comment character '#'.
79
+
80
+ If STR consists of multiple new-line separated lines, the lines
81
+ that start with '#' are removed, and the remaining lines
82
+ returned"
83
+ (s-join "\n" (--remove (string-match "^#" it) (s-split "\n" str))))
84
+
85
+ (defun labrat-view ()
86
+ "View the paragraph at or before point as a label with labrat.
87
+
88
+ This invokes the \"labrat -V\ <label>\" command with the
89
+ paragraph at or before point inserted in the <label> position,
90
+ but with each new-line replaced with the value of the variable
91
+ labrat-nl-sep, '++' by default."
92
+ (interactive)
93
+ (call-process labrat-executable nil nil nil
94
+ "-V" (labrat/pars-in-region)))
95
+
96
+ (defun labrat-print ()
97
+ "Print the paragraph at or before point as a label with labrat.
98
+
99
+ This invokes the \"labrat -P <label>\" command with the paragraph
100
+ at or before point inserted in the <label> position, but with
101
+ each new-line replaced with the value of the variable
102
+ labrat-nl-sep, '++' by default."
103
+ (interactive)
104
+ (call-process labrat-executable nil nil nil
105
+ (labrat/pars-in-region)))
106
+
107
+ (provide 'labrat)
108
+ ;;; labrat.el ends here