Class: MakeTaggable::TagList

Inherits:
Array
  • Object
show all
Defined in:
lib/make_taggable/tag_list.rb

Overview

The list of tag names held against one context of one record.

A tag list is an Array, so everything Array offers works on it. What it adds is parsing, and cleaning: blank entries are dropped, entries are converted to strings and stripped, and duplicates are removed according to the configured case sensitivity.

Examples:

list = MakeTaggable::TagList.new("Fun", "Happy")
list.add("Sad, Lonely", parse: true)
list # => ["Fun", "Happy", "Sad", "Lonely"]

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(*args) ⇒ MakeTaggable::TagList

Builds a tag list from the given names.

Parameters:

  • args (Array<String, Symbol>)

    the tag names, optionally followed by an options hash accepted by #add



36
37
38
# File 'lib/make_taggable/tag_list.rb', line 36

def initialize(*args)
  add(*args)
end

Instance Attribute Details

#ownerActiveRecord::Base, NilClass

The tagger whose tags these are, when the list belongs to an owner.

Returns:

  • (ActiveRecord::Base, NilClass)


25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
# File 'lib/make_taggable/tag_list.rb', line 25

class TagList < Array
  attr_accessor :owner
  attr_writer :parser

  ##
  # Builds a tag list from the given names.
  #
  # @param args [Array<String, Symbol>] the tag names, optionally followed by an options hash
  #   accepted by {#add}
  # @return [MakeTaggable::TagList]
  #
  def initialize(*args)
    add(*args)
  end

  ##
  # The parser this list uses when asked to parse.
  #
  # Falls back to the configured {MakeTaggable.default_parser}, and deliberately does not store
  # it. A stored parser is a Class held in an instance variable, and Psych validates instance
  # variables when dumping, so carrying one made every tag list unserialisable -- `audited`,
  # Active Job arguments and `serialize` columns all refuse it.
  #
  # A parser assigned explicitly is still stored, and a list carrying one is subject to the same
  # limitation.
  #
  # @return [Class]
  #
  def parser
    @parser || MakeTaggable.default_parser
  end

  ##
  # Adds tags to the list, ignoring duplicates and blanks.
  #
  # @param names [Array<String, Symbol>] the tags to add, optionally followed by an options hash
  # @option names [TrueClass, FalseClass] :parse whether to parse the input as a delimited string
  # @option names [Class] :parser a parser to use for this call only
  # @return [MakeTaggable::TagList] self, so calls can be chained
  #
  # @example
  #   tag_list.add("Fun", "Happy")
  #   tag_list.add("Fun, Happy", parse: true)
  #
  def add(*names)
    extract_and_apply_options!(names)
    concat(names)
    clean!
    self
  end

  ##
  # Adds one tag to the list.
  #
  # @param obj [String, Symbol] the tag to add
  # @return [MakeTaggable::TagList] self, so appends can be chained
  #
  def <<(obj)
    add(obj)
  end

  ##
  # Joins two tag lists into a third, leaving both untouched.
  #
  # @param other [Array<String>] the tags to append
  # @return [MakeTaggable::TagList] a new list
  #
  def +(other)
    TagList.new.add(self).add(other)
  end

  ##
  # Appends another list's tags to this one.
  #
  # @param other_tag_list [Array<String>] the tags to append
  # @return [MakeTaggable::TagList] self
  #
  def concat(other_tag_list)
    super.send(:clean!)
    self
  end

  ##
  # Removes tags from the list.
  #
  # @param names [Array<String, Symbol>] the tags to remove, optionally followed by an options
  #   hash accepted by {#add}
  # @return [MakeTaggable::TagList] self
  #
  # @example
  #   tag_list.remove("Sad", "Lonely")
  #   tag_list.remove("Sad, Lonely", parse: true)
  #
  def remove(*names)
    extract_and_apply_options!(names)

    # The list holds strings, so compare strings. Everything else that takes
    # tag names normalises them -- add runs them through clean!, tagged_with
    # parses them -- and a symbol silently matching nothing here was the odd
    # one out.
    names = names.map(&:to_s)

    delete_if { |name| names.include?(name) }
    self
  end

  ##
  # Renders the list as a delimited string, suitable for a form field.
  #
  # Tags containing the delimiter are quoted, so the string parses back into the same list.
  #
  # @return [String]
  #
  # @example
  #   MakeTaggable::TagList.new("Round", "Square,Cube").to_s
  #   # => 'Round, "Square,Cube"'
  #
  def to_s
    tags = frozen? ? dup : self
    tags.send(:clean!)

    delimiter = Regexp.union(Array(MakeTaggable.delimiter))

    tags.map { |name|
      name.index(delimiter) ? "\"#{name}\"" : name
    }.join(MakeTaggable.glue)
  end

  private

  # Convert everything to string, remove whitespace, duplicates, and blanks.
  def clean!
    reject!(&:blank?)
    map!(&:to_s)
    map!(&:strip)
    map!(&:downcase) if MakeTaggable.force_lowercase
    # A name with no ASCII in it parameterizes to "", and reject! below would
    # then drop it -- so the tag vanished rather than being slugged. Keep the
    # original where there is no slug to be had; a caller who wanted strict
    # slugs still gets one wherever one exists.
    map! { |tag| tag.parameterize.presence || tag } if MakeTaggable.force_parameterize

    MakeTaggable.strict_case_match ? uniq! : uniq! { |tag| tag.downcase }
    self
  end

  def extract_and_apply_options!(args)
    options = args.last.is_a?(Hash) ? args.pop : {}
    options.assert_valid_keys :parse, :parser

    chosen_parser = options[:parser] || parser

    args.map! { |a| chosen_parser.new(a).parse } if options[:parse] || options[:parser]

    args.flatten!
  end
end

#parserClass

The parser this list uses when asked to parse.

Falls back to the configured MakeTaggable.default_parser, and deliberately does not store it. A stored parser is a Class held in an instance variable, and Psych validates instance variables when dumping, so carrying one made every tag list unserialisable -- audited, Active Job arguments and serialize columns all refuse it.

A parser assigned explicitly is still stored, and a list carrying one is subject to the same limitation.

Returns:

  • (Class)


25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
# File 'lib/make_taggable/tag_list.rb', line 25

class TagList < Array
  attr_accessor :owner
  attr_writer :parser

  ##
  # Builds a tag list from the given names.
  #
  # @param args [Array<String, Symbol>] the tag names, optionally followed by an options hash
  #   accepted by {#add}
  # @return [MakeTaggable::TagList]
  #
  def initialize(*args)
    add(*args)
  end

  ##
  # The parser this list uses when asked to parse.
  #
  # Falls back to the configured {MakeTaggable.default_parser}, and deliberately does not store
  # it. A stored parser is a Class held in an instance variable, and Psych validates instance
  # variables when dumping, so carrying one made every tag list unserialisable -- `audited`,
  # Active Job arguments and `serialize` columns all refuse it.
  #
  # A parser assigned explicitly is still stored, and a list carrying one is subject to the same
  # limitation.
  #
  # @return [Class]
  #
  def parser
    @parser || MakeTaggable.default_parser
  end

  ##
  # Adds tags to the list, ignoring duplicates and blanks.
  #
  # @param names [Array<String, Symbol>] the tags to add, optionally followed by an options hash
  # @option names [TrueClass, FalseClass] :parse whether to parse the input as a delimited string
  # @option names [Class] :parser a parser to use for this call only
  # @return [MakeTaggable::TagList] self, so calls can be chained
  #
  # @example
  #   tag_list.add("Fun", "Happy")
  #   tag_list.add("Fun, Happy", parse: true)
  #
  def add(*names)
    extract_and_apply_options!(names)
    concat(names)
    clean!
    self
  end

  ##
  # Adds one tag to the list.
  #
  # @param obj [String, Symbol] the tag to add
  # @return [MakeTaggable::TagList] self, so appends can be chained
  #
  def <<(obj)
    add(obj)
  end

  ##
  # Joins two tag lists into a third, leaving both untouched.
  #
  # @param other [Array<String>] the tags to append
  # @return [MakeTaggable::TagList] a new list
  #
  def +(other)
    TagList.new.add(self).add(other)
  end

  ##
  # Appends another list's tags to this one.
  #
  # @param other_tag_list [Array<String>] the tags to append
  # @return [MakeTaggable::TagList] self
  #
  def concat(other_tag_list)
    super.send(:clean!)
    self
  end

  ##
  # Removes tags from the list.
  #
  # @param names [Array<String, Symbol>] the tags to remove, optionally followed by an options
  #   hash accepted by {#add}
  # @return [MakeTaggable::TagList] self
  #
  # @example
  #   tag_list.remove("Sad", "Lonely")
  #   tag_list.remove("Sad, Lonely", parse: true)
  #
  def remove(*names)
    extract_and_apply_options!(names)

    # The list holds strings, so compare strings. Everything else that takes
    # tag names normalises them -- add runs them through clean!, tagged_with
    # parses them -- and a symbol silently matching nothing here was the odd
    # one out.
    names = names.map(&:to_s)

    delete_if { |name| names.include?(name) }
    self
  end

  ##
  # Renders the list as a delimited string, suitable for a form field.
  #
  # Tags containing the delimiter are quoted, so the string parses back into the same list.
  #
  # @return [String]
  #
  # @example
  #   MakeTaggable::TagList.new("Round", "Square,Cube").to_s
  #   # => 'Round, "Square,Cube"'
  #
  def to_s
    tags = frozen? ? dup : self
    tags.send(:clean!)

    delimiter = Regexp.union(Array(MakeTaggable.delimiter))

    tags.map { |name|
      name.index(delimiter) ? "\"#{name}\"" : name
    }.join(MakeTaggable.glue)
  end

  private

  # Convert everything to string, remove whitespace, duplicates, and blanks.
  def clean!
    reject!(&:blank?)
    map!(&:to_s)
    map!(&:strip)
    map!(&:downcase) if MakeTaggable.force_lowercase
    # A name with no ASCII in it parameterizes to "", and reject! below would
    # then drop it -- so the tag vanished rather than being slugged. Keep the
    # original where there is no slug to be had; a caller who wanted strict
    # slugs still gets one wherever one exists.
    map! { |tag| tag.parameterize.presence || tag } if MakeTaggable.force_parameterize

    MakeTaggable.strict_case_match ? uniq! : uniq! { |tag| tag.downcase }
    self
  end

  def extract_and_apply_options!(args)
    options = args.last.is_a?(Hash) ? args.pop : {}
    options.assert_valid_keys :parse, :parser

    chosen_parser = options[:parser] || parser

    args.map! { |a| chosen_parser.new(a).parse } if options[:parse] || options[:parser]

    args.flatten!
  end
end

Instance Method Details

#+(other) ⇒ MakeTaggable::TagList

Joins two tag lists into a third, leaving both untouched.

Parameters:

  • other (Array<String>)

    the tags to append

Returns:



92
93
94
# File 'lib/make_taggable/tag_list.rb', line 92

def +(other)
  TagList.new.add(self).add(other)
end

#<<(obj) ⇒ MakeTaggable::TagList

Adds one tag to the list.

Parameters:

  • obj (String, Symbol)

    the tag to add

Returns:



82
83
84
# File 'lib/make_taggable/tag_list.rb', line 82

def <<(obj)
  add(obj)
end

#add(*names) ⇒ MakeTaggable::TagList

Adds tags to the list, ignoring duplicates and blanks.

Examples:

tag_list.add("Fun", "Happy")
tag_list.add("Fun, Happy", parse: true)

Parameters:

  • names (Array<String, Symbol>)

    the tags to add, optionally followed by an options hash

Options Hash (*names):

  • :parse (TrueClass, FalseClass)

    whether to parse the input as a delimited string

  • :parser (Class)

    a parser to use for this call only

Returns:



69
70
71
72
73
74
# File 'lib/make_taggable/tag_list.rb', line 69

def add(*names)
  extract_and_apply_options!(names)
  concat(names)
  clean!
  self
end

#concat(other_tag_list) ⇒ MakeTaggable::TagList

Appends another list's tags to this one.

Parameters:

  • other_tag_list (Array<String>)

    the tags to append

Returns:



102
103
104
105
# File 'lib/make_taggable/tag_list.rb', line 102

def concat(other_tag_list)
  super.send(:clean!)
  self
end

#remove(*names) ⇒ MakeTaggable::TagList

Removes tags from the list.

Examples:

tag_list.remove("Sad", "Lonely")
tag_list.remove("Sad, Lonely", parse: true)

Parameters:

  • names (Array<String, Symbol>)

    the tags to remove, optionally followed by an options hash accepted by #add

Returns:



118
119
120
121
122
123
124
125
126
127
128
129
# File 'lib/make_taggable/tag_list.rb', line 118

def remove(*names)
  extract_and_apply_options!(names)

  # The list holds strings, so compare strings. Everything else that takes
  # tag names normalises them -- add runs them through clean!, tagged_with
  # parses them -- and a symbol silently matching nothing here was the odd
  # one out.
  names = names.map(&:to_s)

  delete_if { |name| names.include?(name) }
  self
end

#to_sString

Renders the list as a delimited string, suitable for a form field.

Tags containing the delimiter are quoted, so the string parses back into the same list.

Examples:

MakeTaggable::TagList.new("Round", "Square,Cube").to_s
# => 'Round, "Square,Cube"'

Returns:

  • (String)


142
143
144
145
146
147
148
149
150
151
# File 'lib/make_taggable/tag_list.rb', line 142

def to_s
  tags = frozen? ? dup : self
  tags.send(:clean!)

  delimiter = Regexp.union(Array(MakeTaggable.delimiter))

  tags.map { |name|
    name.index(delimiter) ? "\"#{name}\"" : name
  }.join(MakeTaggable.glue)
end