jekyll-page-asset 0.1.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.
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+ module Jekyll
3
+ module PageAsset
4
+ class FountainRenderer < Renderer
5
+ register_type 'fountain'
6
+
7
+ def render(context)
8
+ include_vars = build_include_vars(
9
+ "script_text" => File.read(@target_asset_path)
10
+ )
11
+ render_template(context, include_vars)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "cgi"
4
+
5
+ module Jekyll
6
+ module PageAsset
7
+ class GalleryRenderer < Renderer
8
+ register_type 'gallery'
9
+
10
+ SUPPORTED_IMAGE_EXTENSIONS = %w[.jpg .jpeg .png .webp .avif .gif].freeze
11
+
12
+ def render(context)
13
+ return "" if @target_asset_paths.empty?
14
+
15
+ gallery_asset_root = @asset_path_arg
16
+ images = discover_images(@target_asset_paths)
17
+ gallery_template = render_template(context, {})
18
+
19
+ if images.empty?
20
+ return empty_state_html(@asset_path_arg) + gallery_template
21
+ end
22
+
23
+ thumbnails = images.each_with_index.map do |filename, index|
24
+ asset_path = File.join(gallery_asset_root, filename)
25
+ src = "/#{asset_path}"
26
+ alt = alt_text_for(filename, index)
27
+ picture_tag = render_thumbnail(context, asset_path)
28
+
29
+ <<~HTML
30
+ <button
31
+ class="gallery-thumb"
32
+ type="button"
33
+ data-gallery-thumb="true"
34
+ data-gallery-id="#{html_escape(@asset_path_arg)}"
35
+ data-gallery-index="#{index}"
36
+ data-gallery-src="#{html_escape(src)}"
37
+ data-gallery-alt="#{html_escape(alt)}"
38
+ aria-label="Open image #{index + 1}">
39
+ #{picture_tag}
40
+ </button>
41
+ HTML
42
+ end
43
+
44
+ <<~HTML + gallery_template
45
+ <section class="project-gallery" data-gallery="#{html_escape(@asset_path_arg)}" aria-label="Image gallery">
46
+ <div class="project-gallery-grid">
47
+ #{thumbnails.join("\n")}
48
+ </div>
49
+ </section>
50
+ HTML
51
+ end
52
+
53
+ private
54
+
55
+ def discover_images(paths)
56
+ paths.select { |entry| SUPPORTED_IMAGE_EXTENSIONS.include?(File.extname(entry).downcase) }
57
+ .sort_by(&:downcase)
58
+ end
59
+
60
+ def render_thumbnail(context, path)
61
+ escaped_path = path.gsub('"', '\\"')
62
+ liquid = "{% picture \"#{escaped_path}\" alt=\"thumbnail\" %}"
63
+ Liquid::Template.parse(liquid).render(context)
64
+ end
65
+
66
+ def alt_text_for(filename, index)
67
+ stem = File.basename(filename, File.extname(filename)).tr("_-", " ").strip
68
+ return "Gallery image #{index + 1}" if stem.empty?
69
+
70
+ stem.gsub(/\s+/, " ")
71
+ end
72
+
73
+ def empty_state_html(gallery_id)
74
+ <<~HTML
75
+ <section class="project-gallery" data-gallery="#{html_escape(gallery_id)}" aria-label="Image gallery">
76
+ <p class="project-gallery-empty">No gallery images found.</p>
77
+ </section>
78
+ HTML
79
+ end
80
+
81
+ def html_escape(value)
82
+ CGI.escape_html(value.to_s)
83
+ end
84
+ end
85
+ end
86
+ end
87
+
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "base64"
5
+
6
+ module Jekyll
7
+ module PageAsset
8
+ class ImageRenderer < Renderer
9
+ register_type 'image'
10
+
11
+ def render(context)
12
+ augmented_liquid = "{% picture #{@target_asset_path} #{@rest} %}"
13
+ Liquid::Template.parse(augmented_liquid).render(context)
14
+ end
15
+ end
16
+ end
17
+ end
18
+
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jekyll
4
+ module PageAsset
5
+ class Renderer
6
+ class << self
7
+ attr_reader :type
8
+
9
+ def type_renderers
10
+ @type_renderers ||= {}
11
+ end
12
+
13
+ def inherited(subclass)
14
+ unregistered_subclasses << subclass
15
+ end
16
+
17
+ def unregistered_subclasses
18
+ @unregistered_subclasses ||= []
19
+ end
20
+
21
+ def register_type(value)
22
+ @type = value.to_s.strip
23
+ raise "Render helper #{name} has no registered type" if @type.empty?
24
+
25
+ Renderer.type_renderers[@type] = self
26
+ Renderer.unregistered_subclasses.delete(self)
27
+ end
28
+
29
+ def assert_all_registered!
30
+ missing = unregistered_subclasses.reject { |klass| klass == self }
31
+ return if missing.empty?
32
+
33
+ names = missing.map { |klass| klass.name || klass.to_s }.sort.join(", ")
34
+ raise "PageAsset helpers need to register a type. Check subclass(es): #{names}"
35
+ end
36
+
37
+ def new_by_type(type:, asset_path_arg:, rest:, params:)
38
+ renderer = type_renderers[type.to_s] || self
39
+ renderer.new(
40
+ type: type,
41
+ asset_path_arg: asset_path_arg,
42
+ rest: rest,
43
+ params: params
44
+ )
45
+ end
46
+ end
47
+
48
+ def initialize(type:, asset_path_arg:, rest:, params: )
49
+ @type = type.to_s
50
+ @asset_path_arg = asset_path_arg
51
+ @target_asset_paths = target_asset_paths(asset_path_arg)
52
+ @target_asset_path = @target_asset_paths[0]
53
+ @rest = rest.to_s
54
+ @params = params || {}
55
+ end
56
+
57
+ def render(context)
58
+ include_vars = build_include_vars
59
+ render_template(context, include_vars)
60
+ end
61
+
62
+ protected
63
+
64
+
65
+ def target_asset_paths(target)
66
+ if File.file?(target)
67
+ [target]
68
+ elsif File.directory?(target)
69
+ Dir.children(target)
70
+ .select { |entry| File.file?(File.join(target, entry)) }
71
+ .sort_by(&:downcase)
72
+ else
73
+ # assume the path includes a tag and pass it. This may cause issues
74
+ raise 'target page_asset file not found'
75
+ end
76
+ end
77
+
78
+ def build_include_vars(extra={})
79
+ {
80
+ "asset_paths" => @target_asset_paths,
81
+ "asset_path" => @target_asset_path,
82
+ "rest" => @rest,
83
+ "params" => @params,
84
+ }.merge(extra)
85
+ end
86
+
87
+ def load_template(context)
88
+ site = context.registers[:site]
89
+ include_path = File.join(site.source, "_includes", "page_asset", "#{@type}.html")
90
+
91
+ unless File.exist?(include_path)
92
+ gem_include_path = File.expand_path("../../_includes/page_asset/#{@type}.html", __dir__)
93
+ include_path = gem_include_path if File.exist?(gem_include_path)
94
+ end
95
+
96
+ unless File.exist?(include_path)
97
+ raise IOError, "page_asset: include file not found at #{include_path}"
98
+ end
99
+
100
+ Liquid::Template.parse(File.read(include_path))
101
+ end
102
+
103
+ def render_template(context, include_vars)
104
+ template = load_template(context)
105
+ context.stack do
106
+ context["include"] = include_vars
107
+ template.render(context)
108
+ end
109
+ end
110
+
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Jekyll
4
+ module PageAsset
5
+ class ScrollScrubVideoRenderer < Renderer
6
+ register_type 'scroll_scrub_video'
7
+
8
+ DEFAULTS = {
9
+ "pixels_per_second" => 50,
10
+ "sticky_top" => 120
11
+ }.freeze
12
+
13
+ def render(context)
14
+ include_vars = build_include_vars({
15
+ "video_path" => @target_asset_path,
16
+ "pixels_per_second" => (@params["pixels_per_second"] || DEFAULTS["pixels_per_second"]).to_i,
17
+ "sticky_top" => (@params["sticky_top"] || DEFAULTS["sticky_top"]).to_i
18
+ })
19
+
20
+ render_template(context, include_vars)
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "cgi"
4
+
5
+ module Jekyll
6
+ module PageAsset
7
+ class VideoRenderer < Renderer
8
+ register_type "video"
9
+
10
+ DISPLAY_BACKGROUND = "background"
11
+ DISPLAY_FOREGROUND = "foreground"
12
+
13
+ def render(context)
14
+ video_path = @target_asset_path
15
+
16
+ display = normalized_display(@params["display"])
17
+ classes = display == DISPLAY_BACKGROUND ? "background-video" : "foreground-video"
18
+ playback_attrs = display == DISPLAY_BACKGROUND ? "autoplay muted loop playsinline" : "controls playsinline"
19
+
20
+ <<~HTML
21
+ <video #{playback_attrs} preload="auto" class="#{classes}">
22
+ <source src="/#{html_escape(video_path)}" type="#{html_escape(mime_type_for(video_path))}">
23
+ Your browser does not support the video tag.
24
+ </video>
25
+ HTML
26
+ end
27
+
28
+ private
29
+
30
+ def normalized_display(value)
31
+ normalized = value.to_s.strip.downcase
32
+ return DISPLAY_BACKGROUND if normalized == DISPLAY_BACKGROUND
33
+
34
+ DISPLAY_FOREGROUND
35
+ end
36
+
37
+ def mime_type_for(path)
38
+ case File.extname(path).downcase
39
+ when ".webm"
40
+ "video/webm"
41
+ when ".ogv"
42
+ "video/ogg"
43
+ else
44
+ "video/mp4"
45
+ end
46
+ end
47
+
48
+ def html_escape(value)
49
+ CGI.escape_html(value.to_s)
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../_lib/page_asset/renderer"
4
+
5
+ Dir[File.join(__dir__, "../_lib","page_asset", "*_renderer.rb")].sort.each do |renderer_file|
6
+ require renderer_file
7
+ end
8
+
9
+ Jekyll::PageAsset::Renderer.assert_all_registered!
10
+
11
+ module Jekyll
12
+ module PageAsset
13
+ module TagHelpers
14
+ private
15
+
16
+ def parse_tag_markup(markup, context)
17
+ normalized_markup = markup.to_s.strip
18
+ return [nil, nil, "", {}] if normalized_markup.empty?
19
+
20
+ # expand {{tags}}
21
+ normalized_markup = normalized_markup.gsub(/\{\{\s*([^}]+)\s*\}\}/) do
22
+ variable_path = $1.strip
23
+ # Dynamically lookup the variable in Jekyll's context hierarchy
24
+ context[variable_path].to_s
25
+ end
26
+
27
+ type, target, rest = normalized_markup.split(/\s+/, 3)
28
+ rest ||= ""
29
+
30
+ params = {}
31
+ rest.scan(/(\w+)=(?:'([^']*)'|"([^"]*)"|(\S+))/) do |key, sq, dq, bare|
32
+ params[key] = sq || dq || bare
33
+ end
34
+
35
+ [type, target, rest, params]
36
+ end
37
+
38
+ def tag_site(context)
39
+ context.registers[:site]
40
+ end
41
+
42
+ def tag_page(context)
43
+ context.registers[:page]
44
+ end
45
+
46
+ def project_slug_from_context(context)
47
+ page = tag_page(context)
48
+ page_url = page["url"] || page.url
49
+ page_url.to_s.split("/").reject(&:empty?).last
50
+ end
51
+
52
+ def page_asset_root
53
+ Jekyll.configuration.dig("page_asset", "asset_root") || "assets/project_media"
54
+ end
55
+
56
+ def page_asset_relative_path(context, *segments)
57
+ project_slug = project_slug_from_context(context)
58
+ return nil if project_slug.nil? || project_slug.empty?
59
+
60
+ File.join(page_asset_root, project_slug, *segments.compact)
61
+ end
62
+
63
+ def page_asset_source_path(context, *segments)
64
+ relative_path = page_asset_relative_path(context, *segments)
65
+ return nil if relative_path.nil?
66
+
67
+ File.join(tag_site(context).source, relative_path)
68
+ end
69
+ end
70
+
71
+ class PageAssetTag < Liquid::Tag
72
+ include TagHelpers
73
+
74
+ def initialize(tag_name, markup, options)
75
+ super
76
+ @markup = markup
77
+ end
78
+
79
+ def render(context)
80
+ # Split the markup into file name and opts.
81
+ @type, @target, @rest, @params = parse_tag_markup(@markup, context)
82
+ full_target_path = page_asset_relative_path(context, @target)
83
+ return "" if full_target_path.nil? || full_target_path.empty?
84
+
85
+ renderer = PageAsset::Renderer.new_by_type(
86
+ type: @type,
87
+ asset_path_arg: full_target_path,
88
+ rest: @rest,
89
+ params: @params
90
+ )
91
+ ren = renderer.render(context)
92
+
93
+ <<~HTML
94
+ <div class="page-asset-container">
95
+ #{ren}
96
+ </div>
97
+ HTML
98
+ end
99
+ end
100
+ end
101
+ end
102
+
103
+ Liquid::Template.register_tag("page_asset", Jekyll::PageAsset::PageAssetTag)