jekyll_picture_tag 1.9.0 → 1.12.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (96) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +4 -0
  3. data/.travis.yml +4 -7
  4. data/Dockerfile +9 -0
  5. data/docs/Gemfile +4 -2
  6. data/docs/Gemfile.lock +186 -89
  7. data/docs/_config.yml +6 -10
  8. data/docs/devs/contributing/code.md +44 -0
  9. data/docs/devs/contributing/docs.md +13 -0
  10. data/docs/devs/contributing/index.md +15 -0
  11. data/docs/devs/contributing/setup.md +13 -0
  12. data/docs/devs/contributing/testing.md +41 -0
  13. data/docs/devs/index.md +7 -0
  14. data/docs/{releases.md → devs/releases.md} +35 -13
  15. data/docs/index.md +58 -36
  16. data/docs/users/configuration/cdn.md +35 -0
  17. data/docs/users/configuration/directories.md +34 -0
  18. data/docs/users/configuration/disable.md +24 -0
  19. data/docs/users/configuration/fast_build.md +28 -0
  20. data/docs/users/configuration/ignore_missing.md +23 -0
  21. data/docs/users/configuration/index.md +29 -0
  22. data/docs/users/configuration/kramdown_fix.md +20 -0
  23. data/docs/users/configuration/relative_urls.md +15 -0
  24. data/docs/users/configuration/suppress_warnings.md +16 -0
  25. data/docs/users/index.md +7 -0
  26. data/docs/users/installation.md +52 -0
  27. data/docs/users/liquid_tag/argument_reference/alternate_images.md +18 -0
  28. data/docs/users/liquid_tag/argument_reference/attributes.md +42 -0
  29. data/docs/users/liquid_tag/argument_reference/base_image.md +12 -0
  30. data/docs/users/liquid_tag/argument_reference/crop.md +48 -0
  31. data/docs/users/liquid_tag/argument_reference/link.md +16 -0
  32. data/docs/users/liquid_tag/argument_reference/preset.md +17 -0
  33. data/docs/users/liquid_tag/argument_reference/readme.md +9 -0
  34. data/docs/users/liquid_tag/examples.md +81 -0
  35. data/docs/users/liquid_tag/index.md +31 -0
  36. data/docs/users/notes/git_lfs.md +7 -0
  37. data/docs/users/notes/github_pages.md +5 -0
  38. data/docs/users/notes/html_attributes.md +5 -0
  39. data/docs/users/notes/index.md +6 -0
  40. data/docs/users/notes/input_checking.md +6 -0
  41. data/docs/users/notes/kramdown_bug.md +41 -0
  42. data/docs/users/notes/managing_images.md +21 -0
  43. data/docs/{migration.md → users/notes/migration.md} +0 -0
  44. data/docs/users/presets/cropping.md +61 -0
  45. data/docs/users/presets/default.md +23 -0
  46. data/docs/users/presets/examples.md +79 -0
  47. data/docs/users/presets/fallback_image.md +28 -0
  48. data/docs/users/presets/html_attributes.md +26 -0
  49. data/docs/users/presets/image_formats.md +21 -0
  50. data/docs/users/presets/image_quality.md +43 -0
  51. data/docs/users/presets/index.md +101 -0
  52. data/docs/users/presets/link_source.md +16 -0
  53. data/docs/users/presets/markup_formats/fragments.md +48 -0
  54. data/docs/users/presets/markup_formats/javascript_friendly.md +57 -0
  55. data/docs/users/presets/markup_formats/readme.md +43 -0
  56. data/docs/users/presets/markup_formats/standard_html.md +25 -0
  57. data/docs/users/presets/media_queries.md +36 -0
  58. data/docs/users/presets/nomarkdown_override.md +17 -0
  59. data/docs/users/presets/pixel_ratio_srcsets.md +32 -0
  60. data/docs/users/presets/width_height_attributes.md +34 -0
  61. data/docs/users/presets/width_srcsets.md +85 -0
  62. data/jekyll_picture_tag.gemspec +1 -1
  63. data/lib/jekyll_picture_tag.rb +1 -0
  64. data/lib/jekyll_picture_tag/cache.rb +3 -0
  65. data/lib/jekyll_picture_tag/cache/base.rb +59 -0
  66. data/lib/jekyll_picture_tag/cache/generated.rb +20 -0
  67. data/lib/jekyll_picture_tag/cache/source.rb +19 -0
  68. data/lib/jekyll_picture_tag/defaults/presets.yml +2 -0
  69. data/lib/jekyll_picture_tag/generated_image.rb +52 -41
  70. data/lib/jekyll_picture_tag/img_uri.rb +1 -0
  71. data/lib/jekyll_picture_tag/instructions.rb +1 -0
  72. data/lib/jekyll_picture_tag/instructions/arg_splitter.rb +69 -0
  73. data/lib/jekyll_picture_tag/instructions/configuration.rb +1 -1
  74. data/lib/jekyll_picture_tag/instructions/preset.rb +40 -15
  75. data/lib/jekyll_picture_tag/instructions/set.rb +23 -9
  76. data/lib/jekyll_picture_tag/instructions/tag_parser.rb +59 -69
  77. data/lib/jekyll_picture_tag/output_formats/basic.rb +34 -15
  78. data/lib/jekyll_picture_tag/output_formats/img.rb +11 -0
  79. data/lib/jekyll_picture_tag/output_formats/picture.rb +22 -0
  80. data/lib/jekyll_picture_tag/router.rb +9 -0
  81. data/lib/jekyll_picture_tag/source_image.rb +60 -44
  82. data/lib/jekyll_picture_tag/srcsets/basic.rb +29 -7
  83. data/lib/jekyll_picture_tag/utils.rb +18 -0
  84. data/lib/jekyll_picture_tag/version.rb +1 -1
  85. data/readme.md +40 -16
  86. metadata +67 -20
  87. data/docs/_layouts/directory.html +0 -32
  88. data/docs/assets/style.css +0 -31
  89. data/docs/contributing.md +0 -75
  90. data/docs/example_presets.md +0 -116
  91. data/docs/global_configuration.md +0 -173
  92. data/docs/installation.md +0 -30
  93. data/docs/notes.md +0 -91
  94. data/docs/output.md +0 -63
  95. data/docs/presets.md +0 -309
  96. data/docs/usage.md +0 -113
@@ -0,0 +1,20 @@
1
+ module PictureTag
2
+ module Cache
3
+ # Caches generated image details, so we can skip expensive operations whenever
4
+ # possible.
5
+ # Stored width and height are values for the source image, after cropping.
6
+ class Generated
7
+ include Base
8
+
9
+ private
10
+
11
+ def cache_dir
12
+ 'generated'
13
+ end
14
+
15
+ def template
16
+ { width: nil, height: nil }
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,19 @@
1
+ module PictureTag
2
+ module Cache
3
+ # Caches source image details, so we can skip expensive operations whenever
4
+ # possible.
5
+ class Source
6
+ include Base
7
+
8
+ private
9
+
10
+ def template
11
+ { digest: nil, width: nil, height: nil }
12
+ end
13
+
14
+ def cache_dir
15
+ 'source'
16
+ end
17
+ end
18
+ end
19
+ end
@@ -7,3 +7,5 @@ noscript: false
7
7
  link_source: false
8
8
  quality: 75
9
9
  data_sizes: true
10
+ gravity: center
11
+ dimension_attributes: false
@@ -4,12 +4,15 @@ module PictureTag
4
4
  # Represents a generated image file.
5
5
  class GeneratedImage
6
6
  attr_reader :width, :format
7
+
7
8
  include MiniMagick
8
9
 
9
- def initialize(source_file:, width:, format:)
10
+ def initialize(source_file:, width:, format:, crop: nil, gravity: '')
10
11
  @source = source_file
11
12
  @width = width
12
13
  @format = process_format format
14
+ @crop = crop
15
+ @gravity = gravity
13
16
  end
14
17
 
15
18
  def exists?
@@ -29,7 +32,7 @@ module PictureTag
29
32
  # /home/dave/my_blog/_site/generated/somefolder/myimage-100-123abc.jpg
30
33
  # ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
31
34
  def name
32
- name_left + digest + name_right
35
+ @name ||= "#{@source.base_name}-#{@width}-#{id}.#{@format}"
33
36
  end
34
37
 
35
38
  # https://example.com/assets/images/myimage-100-123abc.jpg
@@ -38,54 +41,64 @@ module PictureTag
38
41
  ImgURI.new(name).to_s
39
42
  end
40
43
 
41
- private
44
+ # Post crop
45
+ def source_width
46
+ update_cache unless cache[:width]
42
47
 
43
- # /home/dave/my_blog/_site/generated/somefolder/myimage-100-123abc.jpg
44
- # ^^^^^^^^^^^^^^^^^^^^^^^
45
- def name_left
46
- "#{@source.base_name}-#{@width}-"
48
+ cache[:width]
47
49
  end
48
50
 
49
- # /home/dave/my_blog/_site/generated/somefolder/myimage-100-123abc.jpg
50
- # ^^^^^^
51
- def digest
52
- guess_digest if !@source.digest_guess && PictureTag.fast_build?
51
+ # Post crop
52
+ def source_height
53
+ update_cache unless cache[:height]
53
54
 
54
- @source.digest
55
+ cache[:height]
55
56
  end
56
57
 
57
- # /home/dave/my_blog/_site/generated/somefolder/myimage-100-123abc.jpg
58
- # ^^^^
59
- def name_right
60
- '.' + @format
58
+ private
59
+
60
+ # We exclude width and format from the cache name, since it isn't specific to them.
61
+ def cache
62
+ @cache ||= Cache::Generated.new("#{@source.base_name}-#{id}")
61
63
  end
62
64
 
63
- # Used for the fast build option: look for a file which matches everything
64
- # we know about the destination file without calculating a digest on the
65
- # source file, and if it exists we assume it's the right one.
66
- def guess_digest
67
- matches = dest_glob
68
- return unless matches.length == 1
65
+ def update_cache
66
+ return if @source.missing
69
67
 
70
- # Start and finish of the destination image's hash value
71
- finish = -name_right.length
72
- start = finish - 6
68
+ # Ensure it's generated:
69
+ image
73
70
 
74
- # The source image keeps track of this guess, so we hand it off:
75
- @source.digest_guess = matches.first[start...finish]
71
+ cache[:width] = @source_dimensions[:width]
72
+ cache[:height] = @source_dimensions[:height]
73
+
74
+ cache.write
76
75
  end
77
76
 
78
- # Returns a list of images which are probably correct.
79
- def dest_glob
80
- Dir.glob File.join(PictureTag.dest_dir, name_left + '?' * 6 + name_right)
77
+ # Hash all inputs and truncate, so we know when they change without getting too long.
78
+ # /home/dave/my_blog/_site/generated/somefolder/myimage-100-1234abcde.jpg
79
+ # ^^^^^^^^^
80
+ def id
81
+ @id ||= Digest::MD5.hexdigest([@source.digest, @crop, @gravity, quality].join)[0..8]
81
82
  end
82
83
 
84
+ # Post crop, before resizing and reformatting
83
85
  def image
84
- @image ||= Image.open(@source.name)
86
+ @image ||= open_image
85
87
  end
86
88
 
87
- def dest_dir
88
- File.dirname absolute_filename
89
+ def open_image
90
+ image_base = Image.open(@source.name)
91
+ image_base.combine_options do |i|
92
+ i.auto_orient
93
+ if @crop
94
+ i.gravity @gravity
95
+ i.crop @crop
96
+ end
97
+ end
98
+
99
+ @source_dimensions = { width: image_base.width, height: image_base.height }
100
+
101
+ image_base
89
102
  end
90
103
 
91
104
  def generate_image
@@ -94,30 +107,28 @@ module PictureTag
94
107
  write_image
95
108
  end
96
109
 
110
+ def quality
111
+ PictureTag.quality(@format)
112
+ end
113
+
97
114
  def process_image
98
115
  image.combine_options do |i|
99
116
  i.resize "#{@width}x"
100
- i.auto_orient
101
117
  i.strip
102
118
  end
103
119
 
104
120
  image.format @format
105
- image.quality PictureTag.quality(@format)
121
+ image.quality quality
106
122
  end
107
123
 
108
124
  def write_image
109
- check_dest_dir
125
+ FileUtils.mkdir_p(File.dirname(absolute_filename))
110
126
 
111
127
  image.write absolute_filename
112
128
 
113
129
  FileUtils.chmod(0o644, absolute_filename)
114
130
  end
115
131
 
116
- # Make sure destination directory exists
117
- def check_dest_dir
118
- FileUtils.mkdir_p(dest_dir) unless Dir.exist?(dest_dir)
119
- end
120
-
121
132
  def process_format(format)
122
133
  if format.casecmp('original').zero?
123
134
  @source.ext
@@ -8,6 +8,7 @@ module PictureTag
8
8
  # image. Call to_s on it to get the link.
9
9
  class ImgURI
10
10
  attr_reader :filename, :source_image
11
+
11
12
  def initialize(filename, source_image: false)
12
13
  @source_image = source_image
13
14
  @filename = filename
@@ -3,3 +3,4 @@ require_relative './instructions/configuration'
3
3
  require_relative './instructions/html_attributes'
4
4
  require_relative './instructions/preset'
5
5
  require_relative './instructions/tag_parser'
6
+ require_relative './instructions/arg_splitter'
@@ -0,0 +1,69 @@
1
+ module PictureTag
2
+ module Instructions
3
+ # This class takes in the arguments passed to the liquid tag, and splits it
4
+ # up into 'words' (correctly handling quotes and backslash escapes.)
5
+ #
6
+ # To handle quotes and backslash escaping, we have to parse the string by
7
+ # characters to break it up correctly. I'm sure there's a library to do
8
+ # this, but it's not that much code honestly. If this starts getting big,
9
+ # we'll pull in a new dependency.
10
+ #
11
+ class ArgSplitter
12
+ attr_reader :words
13
+
14
+ def initialize(raw_params)
15
+ @words = []
16
+ @word = ''
17
+ @in_quotes = false
18
+ @escaped = false
19
+
20
+ raw_params.each_char { |c| handle_char(c) }
21
+
22
+ add_word # We have to explicitly add the last one.
23
+ end
24
+
25
+ private
26
+
27
+ def handle_char(char)
28
+ # last character was a backslash:
29
+ if @escaped
30
+ close_escape char
31
+
32
+ # char is a backslash or a quote:
33
+ elsif char.match?(/["\\]/)
34
+ handle_special char
35
+
36
+ # Character isn't whitespace, or it's inside double quotes:
37
+ elsif @in_quotes || char.match(/\S/)
38
+ @word << char
39
+
40
+ # Character is whitespace outside of double quotes:
41
+ else
42
+ add_word
43
+ end
44
+ end
45
+
46
+ def add_word
47
+ return if @word.empty?
48
+
49
+ @words << @word
50
+ @word = ''
51
+ end
52
+
53
+ def handle_special(char)
54
+ case char
55
+ when '\\'
56
+ @escaped = true
57
+ when '"'
58
+ @in_quotes = !@in_quotes
59
+ @word << char
60
+ end
61
+ end
62
+
63
+ def close_escape(char)
64
+ @word << char
65
+ @escaped = false
66
+ end
67
+ end
68
+ end
69
+ end
@@ -30,7 +30,7 @@ module PictureTag
30
30
  # source_dest is the jekyll-picture-tag destination directory. (generated
31
31
  # file location setting.)
32
32
  def dest_dir
33
- File.join PictureTag.site.dest, pconfig['output']
33
+ File.join PictureTag.site.config['destination'], pconfig['output']
34
34
  end
35
35
 
36
36
  def nomarkdown?
@@ -3,6 +3,7 @@ module PictureTag
3
3
  # Handles the specific tag image set to construct.
4
4
  class Preset
5
5
  attr_reader :name
6
+
6
7
  def initialize(name)
7
8
  @name = name
8
9
  @content = build_preset
@@ -12,13 +13,6 @@ module PictureTag
12
13
  @content[key]
13
14
  end
14
15
 
15
- # Returns the set of widths to use for a given media query.
16
- def widths(media)
17
- width_hash = self['media_widths'] || {}
18
- width_hash.default = self['widths']
19
- width_hash[media]
20
- end
21
-
22
16
  def formats
23
17
  @content['formats']
24
18
  end
@@ -41,15 +35,38 @@ module PictureTag
41
35
  end
42
36
  end
43
37
 
38
+ # Image widths to generate for a given media query.
39
+ def widths(media = nil)
40
+ setting_lookup('widths', 'media', media)
41
+ end
42
+
43
+ # Image quality setting, possibly dependent on format.
44
44
  def quality(format = nil)
45
- qualities = @content['format_quality'] || {}
46
- qualities.default = @content['quality']
45
+ setting_lookup('quality', 'format', format)
46
+ end
47
+
48
+ # Gravity setting (for imagemagick cropping)
49
+ def gravity(media = nil)
50
+ setting_lookup('gravity', 'media', media)
51
+ end
47
52
 
48
- qualities[format]
53
+ # Crop value
54
+ def crop(media = nil)
55
+ setting_lookup('crop', 'media', media)
49
56
  end
50
57
 
51
58
  private
52
59
 
60
+ # Return arbitrary setting values, taking their defaults into account.
61
+ # Ex: quality can be set for all image formats, or individually per
62
+ # format. Per-format settings should override the general setting.
63
+ def setting_lookup(setting, prefix, lookup)
64
+ media_values = @content[prefix + '_' + setting] || {}
65
+ media_values.default = @content[setting]
66
+
67
+ media_values[lookup]
68
+ end
69
+
53
70
  def build_preset
54
71
  # The _data/picture.yml file is optional.
55
72
  picture_data_file = grab_data_file
@@ -64,16 +81,24 @@ module PictureTag
64
81
  end
65
82
 
66
83
  def grab_data_file
84
+ search_data('presets') || search_data('markup_presets') || no_preset
85
+ end
86
+
87
+ def search_data(key)
67
88
  PictureTag.site
68
89
  .data
69
- .dig('picture', 'markup_presets', @name) || no_preset
90
+ .dig('picture', key, @name)
70
91
  end
71
92
 
72
93
  def no_preset
73
- Utils.warning(
74
- " Preset \"#{@name}\" not found in #{PictureTag.config['data_dir']}/"\
75
- + 'picture.yml under markup_presets key. Using default values.'
76
- )
94
+ unless @name == 'default'
95
+ Utils.warning(
96
+ <<~HEREDOC
97
+ Preset "#{@name}" not found in {PictureTag.config['data_dir']}/picture.yml
98
+ under markup_presets key. Using default values."
99
+ HEREDOC
100
+ )
101
+ end
77
102
 
78
103
  {}
79
104
  end
@@ -27,13 +27,35 @@ module PictureTag
27
27
  # These are our Media Query presets. It's really just a hash, and there
28
28
  # are no default values, so extracting this to its own class is overkill.
29
29
  def media_presets
30
- PictureTag.site.data.dig('picture', 'media_presets') || {}
30
+ search_data('media_queries') || search_data('media_presets') || {}
31
31
  end
32
32
 
33
33
  def source_images
34
34
  @source_images ||= build_source_images
35
35
  end
36
36
 
37
+ def crop(media = nil)
38
+ params.geometries[media] || preset.crop(media)
39
+ end
40
+
41
+ def gravity(media = nil)
42
+ params.gravities[media] || preset.gravity(media)
43
+ end
44
+
45
+ # Returns a class constant for the selected output format, which is used
46
+ # to dynamically instantiate it.
47
+ def output_class
48
+ Object.const_get(
49
+ 'PictureTag::OutputFormats::' + Utils.titleize(preset['markup'])
50
+ )
51
+ end
52
+
53
+ private
54
+
55
+ def search_data(key)
56
+ PictureTag.site.data.dig('picture', key)
57
+ end
58
+
37
59
  def build_source_images
38
60
  source_names = params.source_names
39
61
  media_presets = params.media_presets
@@ -48,14 +70,6 @@ module PictureTag
48
70
 
49
71
  sources
50
72
  end
51
-
52
- # Returns a class constant for the selected output format, which is used
53
- # to dynamically instantiate it.
54
- def output_class
55
- Object.const_get(
56
- 'PictureTag::OutputFormats::' + Utils.titleize(preset['markup'])
57
- )
58
- end
59
73
  end
60
74
  end
61
75
  end
@@ -1,105 +1,95 @@
1
1
  module PictureTag
2
2
  module Instructions
3
- # This tag takes the arguments handed to the liquid tag, and extracts the
4
- # preset name (if present), source image name(s), and associated media
5
- # queries (if present). The leftovers (html attributes) are handed off to
6
- # its respective class.
3
+ # Tag Parsing Responsibilities:
4
+ #
5
+ # {% picture mypreset a.jpg 3:2 mobile: b.jpg --alt "Alt" --link "/" %}
6
+ # | Jekyll | TagParser | HTMLAttributes |
7
+ #
8
+ # This class takes the arguments handed to the liquid tag (given as a simple
9
+ # string), hands them to ArgSplitter (which breaks them up into an array of
10
+ # words), extracts the preset name (if present), source image name(s),
11
+ # associated media queries (if present), and image-related arguments such as
12
+ # crop and gravity. HTML attributes are handed off to its respective class
13
+ # (as 'leftovers')
14
+ #
15
+ # Media presets and source names are stored as arrays in their correct
16
+ # orders. Gravities and geometries are stored in a hash, keyed by their
17
+ # relevant media presets. Note that the base image will have a media preset
18
+ # of nil, which is a perfectly fine hash key.
19
+ #
7
20
  class TagParser
8
- attr_reader :preset_name, :source_names, :media_presets
9
- def initialize(raw_params)
10
- build_params PictureTag::Utils.liquid_lookup(raw_params)
21
+ attr_reader :preset_name, :source_names, :media_presets, :gravities,
22
+ :geometries, :leftovers
11
23
 
12
- @preset_name = grab_preset_name
24
+ def initialize(raw_params)
25
+ @raw_params = raw_params
26
+ @params = split_params
13
27
 
14
- # The first param specified will be our base image, so it has no
15
- # associated media query.
16
28
  @media_presets = []
17
- @source_names = [] << strip_quotes(@params.shift)
29
+ @source_names = []
30
+ @geometries = {}
31
+ @gravities = {}
18
32
 
19
- # Detect and store arguments of the format 'media_query: img.jpg' as
20
- # keys and values.
21
- add_media_source while @params.first =~ /[\w\-]+:$/
22
- end
23
-
24
- def leftovers
25
- @params
33
+ parse_params
26
34
  end
27
35
 
28
36
  private
29
37
 
30
- def add_media_source
31
- @media_presets << @params.shift.delete_suffix(':')
32
- @source_names << strip_quotes(@params.shift)
38
+ def split_params
39
+ ArgSplitter
40
+ .new(Utils.liquid_lookup(@raw_params))
41
+ .words
33
42
  end
34
43
 
35
- # First param is the preset name, unless it's a filename.
36
- def grab_preset_name
37
- if @params.first.include? '.'
38
- 'default'
39
- else
40
- @params.shift
41
- end
42
- end
43
-
44
- # Originally separating arguments was just handled by splitting the raw
45
- # params on spaces. To handle quotes and backslash escaping, we have to
46
- # parse the string by characters to break it up correctly. I'm sure
47
- # there's a library to do this, but it's not that much code honestly. If
48
- # this starts getting big, we'll pull in a new dependency.
49
- def build_params(raw_params)
50
- @params = []
51
- @word = ''
52
- @in_quotes = false
53
- @escaped = false
44
+ def parse_params
45
+ @preset_name = determine_preset_name
46
+ @source_names << strip_quotes(@params.shift)
54
47
 
55
- raw_params.each_char { |c| handle_char(c) }
48
+ parse_param(@params.first) until stop_here? @params.first
56
49
 
57
- add_word # We have to explicitly add the last one.
50
+ @leftovers = @params
58
51
  end
59
52
 
60
- def handle_char(char)
61
- # last character was a backslash:
62
- if @escaped
63
- close_escape char
53
+ def parse_param(param)
54
+ if param.match?(/[\w\-]+:$/)
55
+ add_media_source
64
56
 
65
- # char is a backslash or a quote:
66
- elsif char.match(/["\\]/)
67
- handle_special char
57
+ elsif Utils::GRAVITIES.include?(param.downcase)
58
+ @gravities[@media_presets.last] = @params.shift
68
59
 
69
- # Character isn't whitespace, or it's inside double quotes:
70
- elsif @in_quotes || char.match(/\S/)
71
- @word << char
60
+ elsif param.match?(Utils::GEOMETRY_REGEX)
61
+ @geometries[@media_presets.last] = @params.shift
72
62
 
73
- # Character is whitespace outside of double quotes:
74
63
  else
75
- add_word
64
+ raise_error(param)
76
65
  end
77
66
  end
78
67
 
79
- def handle_special(char)
80
- if char == '\\'
81
- @escaped = true
82
- elsif char == '"'
83
- @in_quotes = !@in_quotes
84
- @word << char
85
- end
68
+ # HTML attributes are handled by its own class; once we encounter them
69
+ # we are finished here.
70
+ def stop_here?(param)
71
+ # No param Explicit HTML attribute Implicit HTML attribute
72
+ param.nil? || param.match?(/^--\S*/) || param.match?(/^\w*="/)
86
73
  end
87
74
 
88
- def add_word
89
- return if @word.empty?
90
-
91
- @params << @word
92
- @word = ''
75
+ def add_media_source
76
+ @media_presets << @params.shift.delete_suffix(':')
77
+ @source_names << strip_quotes(@params.shift)
93
78
  end
94
79
 
95
- def close_escape(char)
96
- @word << char
97
- @escaped = false
80
+ # The first param is the preset name, unless it's a filename.
81
+ def determine_preset_name
82
+ @params.first.include?('.') ? 'default' : @params.shift
98
83
  end
99
84
 
100
85
  def strip_quotes(name)
101
86
  name.delete_prefix('"').delete_suffix('"')
102
87
  end
88
+
89
+ def raise_error(param)
90
+ raise ArgumentError, "Could not parse '#{param}' in the following "\
91
+ "tag: \n {% picture #{@raw_params} %}"
92
+ end
103
93
  end
104
94
  end
105
95
  end