ragerender 0.1.4 → 0.1.6

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 87b99ec72e739c37527a89acfc8124efb5ed6f293446ee36ae84ffdc072f061b
4
- data.tar.gz: cb2735417a459577704a526ff90b4723709101faca6ec4b7d927edf598dfc697
3
+ metadata.gz: 313e3cd9c0ccd4c11ea547a565e2a1cfa77af1fda5e2fc2219c0da8046c6f27a
4
+ data.tar.gz: 3911407ca12e719f71396099f73d70c6b19aeee29ccc38f796ef4c45b684d80d
5
5
  SHA512:
6
- metadata.gz: 45d46421b5e611dbb14debb3e290a89dd50b38586c843d51da42557cf87402fc7a9afe354680e7cd5ca8cc05a33e7c338605fc1e0526190a848c64b5288315c1
7
- data.tar.gz: c3f33f3eeb53ad63cfe135acd394a838446dd6270388605fa299f3d303ba6fb3876844b569b0b611b27f2c5d641a4d2256067b29863fdd60a97fffc8bc6074ea
6
+ metadata.gz: 4ca0ef393683e910b68e9ffe41a5909511ec66da604b50bcb99fbd758cad371bdeafdd594e1a27394e461beb3e0e8f41c4c3033a28f4c7692a492b470251f8b2
7
+ data.tar.gz: 5509097f24aa3df45416be46acb9d2f31710318b4227b190f9413dcec287d6c473793bff0fd08b3ecf7b1cc49617b9437739fc991d1c9bd2a309fcbd61eef199
data/README.rdoc CHANGED
@@ -60,10 +60,14 @@ The file name of the image will be the title of your comic page. And that's it,
60
60
  you added your first comic!
61
61
 
62
62
  If you want to add an author note, create a text file in a folder called
63
- <tt>_comics</tt> that has the same file name, but with a <tt>.md</tt> extension:
63
+ <tt>_comics</tt> that has the same file name, but with a <tt>.txt</tt> extension:
64
64
 
65
65
  mkdir _comics
66
- echo "Check out my cool comic y'all!" > '_comics/My first page.md'
66
+ echo "Check out my cool comic y'all!" > '_comics/My first page.txt'
67
+
68
+ Or use HTML:
69
+
70
+ echo "This is my <strong>first</strong> page!" > '_comics/My first page.html'
67
71
 
68
72
  Generate the site using:
69
73
 
@@ -101,18 +105,17 @@ Your webcomic now has its basic information set up.
101
105
 
102
106
  === Adding your layouts
103
107
 
104
- RageRender will use a ComicFury default layout if you don't supply your own
105
- files.
108
+ If you want to use your own layout code, then create a <tt>_layouts</tt>
109
+ directory and put the contents of each of your ComicFury layout tabs in there,
110
+ and then put your CSS in the main folder.
106
111
 
107
- If you want to keep using the "simple" layout, you can add the details into your
108
- <tt>_config.yml</tt> too:
112
+ The easiest way is to go to your Webcomic Management, click "Edit Layout", then
113
+ in the box labelled "Useful", click "Download Layout Backup". Pass this file to
114
+ RageRender, which will <tt>unpack</tt> it for you:
109
115
 
110
- layout:
116
+ bundle exec jekyll unpack mycomic-2025-09-13.cflxml
111
117
 
112
- If you want to use your own layout code, then create a <tt>_layouts</tt>
113
- directory and put the contents of each of your ComicFury layout tabs in there,
114
- and then put your CSS in the main folder. You should end up with a full set of
115
- files like:
118
+ You should end up with a full set of files like:
116
119
 
117
120
  _layouts
118
121
  archive.html
@@ -130,7 +133,7 @@ instead.
130
133
 
131
134
  === Adding blogs
132
135
 
133
- Add your blogs into a folder called `_posts`:
136
+ Add your blogs into a folder called <tt>_posts</tt>:
134
137
 
135
138
  cat _posts/2025-05-29-my-new-comic.md
136
139
  Hey guys, welcome to my new comic! It's gonna be so sick!
@@ -197,6 +200,16 @@ You control this by setting a <tt>frontpage</tt> key in your site config.
197
200
  - anything else will display the extra page that has the matching
198
201
  <tt>slug</tt> in its Front Matter
199
202
 
203
+ === Putting changes on ComicFury
204
+
205
+ Once you're done making changes, you can <tt>pack</tt> your layout:
206
+
207
+ bundle exec jekyll pack
208
+
209
+ The resulting file can be uploaded to ComicFury by going to your Webcomic
210
+ Management, clicking "Edit Layout", then in the box labelled "Useful", click
211
+ "Restore Layout Backup".
212
+
200
213
  === Stuff that doesn't work
201
214
 
202
215
  Here is a probably incomplete list of things you can expect to be different
@@ -0,0 +1,105 @@
1
+ require 'pathname'
2
+ require 'rexml'
3
+ require 'base64'
4
+
5
+ module RageRender
6
+ HTML_FILES = {
7
+ overall: 'overall',
8
+ overview: 'overview',
9
+ viewblog: 'blog-display',
10
+ comic: 'comic-page',
11
+ archive: 'archive',
12
+ blogarchive: 'blog-archive',
13
+ error: 'error-page',
14
+ search: 'search',
15
+ }
16
+
17
+ CSS_FILES = {
18
+ layoutcss: 'layout'
19
+ }
20
+
21
+ def self.unpack src, html_dest, css_dest
22
+ doc = REXML::Document.new(src).elements
23
+ html_dir = Pathname.new(html_dest)
24
+ css_dir = Pathname.new(css_dest)
25
+
26
+ HTML_FILES.each do |(tag, filename)|
27
+ File.write html_dir.join(filename + '.html'), Base64.decode64(doc["/layout/ldata/#{tag.to_s}/text()"].value)
28
+ end
29
+
30
+ CSS_FILES.each do |(tag, filename)|
31
+ File.write css_dir.join(filename + '.css'), Base64.decode64(doc["/layout/ldata/#{tag.to_s}/text()"].value)
32
+ end
33
+ end
34
+
35
+ def self.pack html_srcdir, css_srcdir, dest
36
+ layout = REXML::Element.new('layout')
37
+
38
+ name = REXML::Element.new('name')
39
+ name.text = "Downloaded ComicFury layout"
40
+ layout.add name
41
+
42
+ version = REXML::Element.new('cfxml')
43
+ version.text = '1.2'
44
+ layout.add version
45
+
46
+ spage = REXML::Element.new('spage')
47
+ spage.text = '1'
48
+ layout.add spage
49
+
50
+ ldata = REXML::Element.new('ldata')
51
+ html_dir = Pathname.new(html_srcdir)
52
+ css_dir = Pathname.new(css_srcdir)
53
+
54
+ HTML_FILES.each do |(tag, filename)|
55
+ elem = REXML::Element.new tag.to_s
56
+ elem.text = Base64.strict_encode64 File.read html_dir.join(filename + '.html')
57
+ ldata.add elem
58
+ end
59
+
60
+ CSS_FILES.each do |(tag, filename)|
61
+ elem = REXML::Element.new tag.to_s
62
+ elem.text = Base64.strict_encode64 File.read css_dir.join(filename + '.css')
63
+ ldata.add elem
64
+ end
65
+ layout.add ldata
66
+
67
+ # ComicFury will only accept backup files that have:
68
+ # - A valid XML declaration using double quotes
69
+ # – A comment as per below
70
+ # – Tab indentation
71
+ doc = REXML::Document.new(nil, prologue_quote: :quote)
72
+ doc.add REXML::XMLDecl.new REXML::XMLDecl::DEFAULT_VERSION, REXML::XMLDecl::DEFAULT_ENCODING
73
+ doc.add REXML::Comment.new 'This is a ComicFury layout backup, use the import layout function to restore this to a webcomic site. You can access this as follows: Go to your Webcomic Management, click "Edit Layout", then in the Box labelled "Useful", click "Restore Layout Backup"'
74
+ doc.add layout
75
+
76
+ # Pretty print, but *always* make text nodes take up a single line, no
77
+ # matter how long they are
78
+ formatter = REXML::Formatters::Pretty.new(1)
79
+ formatter.compact = true
80
+ formatter.width = Float::INFINITY
81
+
82
+ # Replace the space indentation with tab indentation
83
+ buf = StringIO.new
84
+ formatter.write(doc, buf)
85
+ buf.string.each_line do |line|
86
+ dest << line.gsub(/^ +/) {|sp| "\t" * sp.size }
87
+ end
88
+ end
89
+ end
90
+
91
+ if __FILE__ == $0
92
+ case ARGV.shift
93
+ when 'pack'
94
+ html_srcdir, css_srcdir, *dest = ARGV
95
+ RageRender.pack(html_srcdir, css_srcdir, dest.any? ? File.open(dest.first, 'w') : $stdout)
96
+ when 'unpack'
97
+ src, html_dest, css_dest = ARGV
98
+ RageRender.unpack(src == '-' ? $stdin : File.open(src, 'r'), html_dest, css_dest)
99
+ else
100
+ raise <<~USAGE
101
+ Usage: pack html_srcdir css_srcdir dest
102
+ Usage: unpack src html_dest css_dest
103
+ USAGE
104
+ end
105
+ end
@@ -1,17 +1,25 @@
1
1
  require 'cgi'
2
+ require 'json'
2
3
 
3
4
  # Include this module to get ComicFury intrinsic functions available in
4
5
  # templates.
5
6
  module RageRender
6
7
  module TemplateFunctions
7
8
  def js str
8
- '"' + CGI.escape_html(str) + '"'
9
+ JSON.generate(str, script_safe: true, ascii_only: true).gsub(/[<>'&]|\\"/) do |m|
10
+ '\u' + sprintf('%04x', m.chars.last.ord).upcase
11
+ end
9
12
  end
10
13
 
11
14
  def randomnumber a, b
12
15
  rand a.to_i..b.to_i
13
16
  end
14
17
 
18
+ # Unescape all HTML entities in the input
19
+ def rawhtml str
20
+ CGI.unescape_html str
21
+ end
22
+
15
23
  # https://github.com/Shopify/liquid/blob/9bb7fbf123e6e2bd61e00189b1c83159f375d3f3/lib/liquid/standardfilters.rb#L24-L29
16
24
  # Used under the MIT License.
17
25
  STRIP_HTML_BLOCKS = Regexp.union(
@@ -2,12 +2,13 @@ require 'jekyll/generator'
2
2
  require 'jekyll/drops/drop'
3
3
  require_relative 'comics'
4
4
  require_relative 'pagination'
5
- require_relative 'named_data_delegator'
5
+ require_relative 'pipettes'
6
6
 
7
7
  # Pass the right variables to archive pages. Note that this doesn't apply to
8
8
  # chapter pages because they are not "pages"
9
9
  Jekyll::Hooks.register :pages, :pre_render do |page, payload|
10
10
  if page.data['layout'] == 'archive'
11
+ RageRender::Pipettes.clean_payload payload
11
12
  payload.merge! RageRender::ArchiveDrop.new(page).to_liquid
12
13
  end
13
14
  end
@@ -98,7 +99,7 @@ module RageRender
98
99
  # A Drop that provides all of the page variables for the archive pages.
99
100
  class ArchiveDrop < Jekyll::Drops::Drop
100
101
  private delegate_method_as :data, :fallback_data
101
- extend NamedDataDelegator
102
+ extend Pipettes
102
103
 
103
104
  def ischapterarchive
104
105
  @obj.type == :chapters
@@ -1,20 +1,27 @@
1
1
  require 'jekyll/hooks'
2
2
  require 'jekyll/drops/document_drop'
3
3
  require_relative '../date_formats'
4
- require_relative 'named_data_delegator'
4
+ require_relative 'pipettes'
5
5
 
6
6
  Jekyll::Hooks.register :posts, :pre_render do |post, payload|
7
+ RageRender::Pipettes.clean_payload payload
7
8
  payload.merge! RageRender::BlogDrop.new(post).to_liquid
8
9
  end
9
10
 
10
11
  module RageRender
11
12
  class BlogDrop < Jekyll::Drops::DocumentDrop
12
13
  private delegate_method_as :data, :fallback_data
13
- extend NamedDataDelegator
14
+ extend Pipettes
14
15
 
15
- def_data_delegator :title, :blogtitle
16
16
  def_data_delegator :author, :authorname
17
- delegate_method_as :content, :blog
17
+
18
+ def blogtitle
19
+ escape @obj.data['title']
20
+ end
21
+
22
+ def blog
23
+ maybe_escape @obj.content
24
+ end
18
25
 
19
26
  def posttime
20
27
  comicfury_date @obj.date
@@ -3,11 +3,12 @@ require 'jekyll/drops/drop'
3
3
  require 'jekyll/drops/document_drop'
4
4
  require_relative '../date_formats'
5
5
  require_relative 'pagination'
6
- require_relative 'named_data_delegator'
6
+ require_relative 'pipettes'
7
7
 
8
8
  # Pass the right variables to blog archive pages.
9
9
  Jekyll::Hooks.register :pages, :pre_render do |page, payload|
10
10
  if page.data['layout'] == 'blog-archive'
11
+ RageRender::Pipettes.clean_payload payload
11
12
  payload.merge! RageRender::BlogArchiveDrop.new(page).to_liquid
12
13
  end
13
14
  end
@@ -90,7 +91,7 @@ module RageRender
90
91
  # Data representing a single paginated blog entry, as available from
91
92
  # [l:blogs_paginated].
92
93
  class PaginatedBlogDrop < Jekyll::Drops::DocumentDrop
93
- extend NamedDataDelegator
94
+ extend Pipettes
94
95
 
95
96
  def_data_delegator :title, :blogtitle
96
97
  def_delegator :@obj, :url, :bloglink
@@ -1,6 +1,6 @@
1
1
  require 'jekyll/generator'
2
2
  require 'jekyll/drops/document_drop'
3
- require_relative 'named_data_delegator'
3
+ require_relative 'pipettes'
4
4
  require_relative 'setup_collection'
5
5
 
6
6
  # Add default values for the 'unchapter' which is used to hold all comics that
@@ -21,6 +21,7 @@ Jekyll::Hooks.register :site, :after_init do |site|
21
21
  end
22
22
 
23
23
  Jekyll::Hooks.register :chapters, :pre_render do |chapter, payload|
24
+ RageRender::Pipettes.clean_payload payload
24
25
  payload.merge! RageRender::ChapterDrop.new(chapter).to_liquid
25
26
  payload.merge! RageRender::ArchiveDrop.new(chapter).to_liquid
26
27
  end
@@ -76,15 +77,15 @@ module RageRender
76
77
  PAGINATION_FIELDS = %w[ chaptername chapterdescription ]
77
78
 
78
79
  delegate_method_as :data, :fallback_data
79
- extend NamedDataDelegator
80
+ extend Pipettes
80
81
  extend Forwardable
81
82
 
82
- def_data_delegator :title, :chaptername
83
83
  def_data_delegator :description, :chapterdescription
84
+ def_data_delegator :image, :image
84
85
  def_delegator :@obj, :url, :chapterarchiveurl
85
86
 
86
- def cover
87
- cover_obj.url
87
+ def chaptername
88
+ escape @obj.data['title']
88
89
  end
89
90
 
90
91
  def cover_width_small
@@ -114,24 +115,15 @@ module RageRender
114
115
  end
115
116
 
116
117
  private
117
- def cover_width
118
- cover_obj.data['width'] ||= Dimensions.width cover_obj.path
119
- end
120
-
121
- def cover_height
122
- cover_obj.data['height'] ||= Dimensions.height cover_obj.path
123
- end
124
-
125
- def cover_obj
126
- @cover_obj ||= @obj.site.static_files.detect {|f| f.relative_path == cover_relative_path }
127
- end
128
-
129
- def cover_relative_path
130
- Pathname.new('/').join(@obj.data['image']).to_s
131
- end
118
+ def_image_metadata :image
132
119
 
133
120
  def first_comic
134
121
  @obj.site.collections['comics'].docs.select {|c| c.data['chapter'] == @obj.data['slug'] }.first
135
122
  end
123
+
124
+ public
125
+ alias cover image_url
126
+ alias cover_height image_height
127
+ alias cover_width image_width
136
128
  end
137
129
  end
@@ -2,9 +2,10 @@ require 'jekyll/generator'
2
2
  require 'jekyll/document'
3
3
  require 'jekyll/drops/document_drop'
4
4
  require_relative '../date_formats'
5
- require_relative 'named_data_delegator'
5
+ require_relative 'pipettes'
6
6
 
7
7
  Jekyll::Hooks.register :comics, :pre_render do |page, payload|
8
+ RageRender::Pipettes.clean_payload payload
8
9
  payload.merge! RageRender::ComicDrop.new(page).to_liquid
9
10
  end
10
11
 
@@ -76,16 +77,20 @@ module RageRender
76
77
  end
77
78
 
78
79
  class ComicDrop < Jekyll::Drops::DocumentDrop
79
- extend NamedDataDelegator
80
+ extend Pipettes
80
81
 
81
82
  PAGINATION_FIELDS = %w[ comicurl comictitle posttime ]
82
83
 
83
84
  delegate_method_as :id, :comicid
84
- def_data_delegator :title, :comictitle
85
85
  def_delegator :@obj, :url, :comicurl
86
+ def_delegator :@obj, :url, :permalink
86
87
  data_delegator 'rating'
87
88
  data_delegator 'votecount'
88
89
 
90
+ def comictitle
91
+ escape @obj.data['title']
92
+ end
93
+
89
94
  def posttime
90
95
  comicfury_date(@obj.date)
91
96
  end
@@ -99,11 +104,11 @@ module RageRender
99
104
  end
100
105
 
101
106
  def chaptername
102
- chapter.data['title']
107
+ escape(chapter.data['title']) rescue nil
103
108
  end
104
109
 
105
110
  def chapterlink
106
- chapter.url
111
+ chapter&.url
107
112
  end
108
113
 
109
114
  def dropdown
@@ -118,7 +123,7 @@ module RageRender
118
123
  dropdown << {
119
124
  'is_selected' => @obj == c,
120
125
  'is_disabled' => false,
121
- 'title' => c.data['title'],
126
+ 'title' => escape(c.data['title']),
122
127
  'grouplabel' => c.data['chapter'],
123
128
  'newgroup' => new_group,
124
129
  'endgroup' => false,
@@ -141,7 +146,7 @@ module RageRender
141
146
  def authornotes
142
147
  @obj.data['authornotes'] || [{
143
148
  'is_reply' => false,
144
- 'comment' => @obj.content,
149
+ 'comment' => maybe_escape(@obj.content),
145
150
  'isguest' => false,
146
151
  'avatar' => nil,
147
152
  'authorname' => @obj.data['author'],
@@ -152,8 +157,12 @@ module RageRender
152
157
  end
153
158
 
154
159
  def custom
155
- @obj.data.fetch('custom', {}).reject do |k, v|
160
+ chapter_data = chapter.nil? ? {} : chapter.data.fetch('custom', {})
161
+ comic_data = @obj.data.fetch('custom', {})
162
+ chapter_data.merge(comic_data).reject do |k, v|
156
163
  v.nil? || (v.respond_to?(:empty?) && v.empty?)
164
+ end.transform_values do |v|
165
+ v.is_a?(String) ? escape(v) : v
157
166
  end
158
167
  end
159
168
 
@@ -173,18 +182,6 @@ module RageRender
173
182
  @obj.next_doc&.url
174
183
  end
175
184
 
176
- def comicimageurl
177
- File.join (@obj.site.baseurl || ''), image_relative_path
178
- end
179
-
180
- def comicwidth
181
- image_obj.data['width'] ||= Dimensions.width image_path
182
- end
183
-
184
- def comicheight
185
- image_obj.data['height'] ||= Dimensions.height image_path
186
- end
187
-
188
185
  # An HTML tag to print for the comic image. If there is a future image, then
189
186
  # this is also a link to the next comic page.
190
187
  def comicimage
@@ -215,16 +212,12 @@ module RageRender
215
212
  @obj.site.collections['chapters'].docs.detect {|c| c.data['slug'] == @obj.data['chapter'] }
216
213
  end
217
214
 
218
- def image_obj
219
- @image_obj ||= @obj.site.static_files.detect {|f| f.relative_path == image_relative_path }
220
- end
221
-
222
- def image_path
223
- image_obj.path
224
- end
215
+ data_delegator 'image'
216
+ def_image_metadata :image
225
217
 
226
- def image_relative_path
227
- Pathname.new('/').join(@obj.data['image']).to_s
228
- end
218
+ public
219
+ alias comicimageurl image_url
220
+ alias comicwidth image_width
221
+ alias comicheight image_height
229
222
  end
230
223
  end
@@ -1,7 +1,8 @@
1
- require_relative 'named_data_delegator'
1
+ require_relative 'pipettes'
2
2
 
3
3
  Jekyll::Hooks.register :pages, :pre_render do |page, payload|
4
4
  if page.data['layout'] == 'error-page'
5
+ RageRender::Pipettes.clean_payload payload
5
6
  payload.merge! RageRender::ErrorDrop.new(page).to_liquid
6
7
  end
7
8
  end
@@ -9,7 +10,7 @@ end
9
10
  module RageRender
10
11
  class ErrorDrop < Jekyll::Drops::Drop
11
12
  private delegate_method_as :data, :fallback_data
12
- extend NamedDataDelegator
13
+ extend Pipettes
13
14
  extend Forwardable
14
15
 
15
16
  def_data_delegator :title, :errortitle
@@ -6,6 +6,7 @@ require_relative 'blog_archive'
6
6
  # Pass the right variables to overview pages.
7
7
  Jekyll::Hooks.register :pages, :pre_render do |page, payload|
8
8
  if page.data['layout'] == 'overview'
9
+ RageRender::Pipettes.clean_payload payload
9
10
  payload.merge! RageRender::ComicDrop.new(page.site.collections['comics'].docs.last).to_liquid
10
11
  payload.merge! RageRender::OverviewDrop.new(page).to_liquid
11
12
  end
@@ -0,0 +1,61 @@
1
+ # Pipettes help you make drops.
2
+ require 'cgi'
3
+ require 'dimensions'
4
+
5
+ module RageRender
6
+ module Pipettes
7
+ def self.clean_payload payload
8
+ Jekyll.logger.debug("Cleaning payload")
9
+ sets = Jekyll::Drops::DocumentDrop.subclasses.map(&:invokable_methods)
10
+ methods = sets.reduce(Set.new) {|s,acc| acc.merge(s)} - Set.new(Jekyll::Drops::DocumentDrop.invokable_methods)
11
+ payload.send(:fallback_data).delete_if {|k| methods.include? k}
12
+ end
13
+
14
+ def def_data_delegator key, aliaz
15
+ define_method(aliaz.to_sym) do
16
+ @obj.data[key.to_s]
17
+ end
18
+ end
19
+
20
+ def self.extended mod
21
+ mod.define_method(:escape) do |str|
22
+ str.nil? ? nil : CGI.escapeHTML(str)
23
+ end
24
+ mod.send(:private, :escape)
25
+
26
+ mod.define_method(:maybe_escape) do |str|
27
+ Pathname.new(@obj.path).extname != '.html' ? escape(str) : str
28
+ end
29
+ mod.send(:private, :maybe_escape)
30
+ end
31
+
32
+ def def_image_metadata prefix
33
+ define_method(:"#{prefix}_relative_path") do
34
+ Pathname.new('/').join(send(prefix.to_sym)).to_path
35
+ end
36
+
37
+ define_method(:"#{prefix}_url") do
38
+ File.join (@obj.site.baseurl || ''), send(:"#{prefix}_relative_path")
39
+ end
40
+
41
+ define_method(:"#{prefix}_obj") do
42
+ unless instance_variable_defined? :"@#{prefix}_obj"
43
+ instance_variable_set(:"@#{prefix}_obj", @obj.site.static_files.detect {|f| f.relative_path == send(:"#{prefix}_relative_path") })
44
+ end
45
+ instance_variable_get(:"@#{prefix}_obj")
46
+ end
47
+
48
+ define_method(:"#{prefix}_path") do
49
+ send(:"#{prefix}_obj").path
50
+ end
51
+
52
+ define_method(:"#{prefix}_width") do
53
+ send(:"#{prefix}_obj").data['width'] ||= Dimensions.width(send(:"#{prefix}_path")) rescue nil
54
+ end
55
+
56
+ define_method(:"#{prefix}_height") do
57
+ send(:"#{prefix}_obj").data['height'] ||= Dimensions.height(send(:"#{prefix}_path")) rescue nil
58
+ end
59
+ end
60
+ end
61
+ end
@@ -1,5 +1,6 @@
1
1
  Jekyll::Hooks.register :pages, :pre_render do |page, payload|
2
2
  if page.data['layout'] == 'search'
3
+ RageRender::Pipettes.clean_payload payload
3
4
  payload.merge! RageRender::SearchDrop.new(page).to_liquid
4
5
  end
5
6
  end
@@ -1,11 +1,11 @@
1
1
  require 'etc'
2
2
  require 'stringio'
3
3
  require 'jekyll'
4
- require 'dimensions'
5
4
  require_relative 'language'
6
5
  require_relative 'functions'
7
6
  require_relative 'to_liquid'
8
7
  require_relative 'date_formats'
8
+ require_relative 'cflxml'
9
9
  require_relative 'jekyll/archive'
10
10
  require_relative 'jekyll/blog'
11
11
  require_relative 'jekyll/blog_archive'
@@ -14,9 +14,42 @@ require_relative 'jekyll/chapter'
14
14
  require_relative 'jekyll/overview'
15
15
  require_relative 'jekyll/error'
16
16
  require_relative 'jekyll/search'
17
- require_relative 'jekyll/named_data_delegator'
17
+ require_relative 'jekyll/pipettes'
18
18
  require_relative 'jekyll/setup_collection'
19
19
 
20
+ module RageRender
21
+ class CFLXMLCommands < Jekyll::Command
22
+ class << self
23
+ def init_with_program(jekyll)
24
+ jekyll.command :pack do |pack|
25
+ pack.syntax 'pack'
26
+ pack.description 'Create a ComicFury layout backup from this site'
27
+ pack.action do |args, options|
28
+ config = configuration_from_options(options)
29
+ filename = "#{(config['title'] || File.basename(config['source']))} #{Time.now.to_s}.cflxml"
30
+ puts "Outputting backup file #{filename}"
31
+ File.open(filename, 'w') do |file|
32
+ RageRender.pack config['layouts_dir'], config['source'], file
33
+ end
34
+ end
35
+ end
36
+
37
+ jekyll.command :unpack do |unpack|
38
+ unpack.syntax 'unpack <file>'
39
+ unpack.description 'Overwrite HTML and CSS from a ComicFury layout backup'
40
+ unpack.action do |args, options|
41
+ raise ArgumentError, 'unpack requires one cflxml file' unless args.one?
42
+ config = configuration_from_options(options)
43
+ File.open(args.first, 'r') do |file|
44
+ RageRender.unpack file, config['layouts_dir'], config['source']
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
52
+
20
53
  Jekyll::Hooks.register :site, :after_init do |site|
21
54
  # This is obviously quite naughty for many reasons,
22
55
  # but it's the only way to get the theme selected
@@ -84,24 +117,31 @@ Jekyll::Hooks.register :documents, :pre_render do |doc, payload|
84
117
  end
85
118
 
86
119
  Jekyll::Hooks.register :pages, :pre_render do |page, payload|
120
+ RageRender::Pipettes.clean_payload payload
87
121
  payload.merge! RageRender::WebcomicDrop.new(page).to_liquid
88
122
  end
89
123
 
90
124
  class RageRender::WebcomicDrop < Jekyll::Drops::Drop
91
125
  extend Forwardable
92
- extend RageRender::NamedDataDelegator
126
+ extend RageRender::Pipettes
93
127
 
94
128
  def self.def_config_delegator source, target
95
129
  define_method(target) { @obj.site.config[source.to_s] }
96
130
  end
97
131
 
98
- def_config_delegator :title, :webcomicname
99
- def_config_delegator :description, :webcomicslogan
100
132
  def_config_delegator :search, :searchon
101
133
  %w{bannerads allowratings showpermalinks showcomments allowcomments}.each do |var|
102
134
  def_config_delegator var, var
103
135
  end
104
136
 
137
+ def webcomicname
138
+ escape @obj.site.config['title']
139
+ end
140
+
141
+ def webcomicslogan
142
+ escape @obj.site.config['description']
143
+ end
144
+
105
145
  def webcomicurl
106
146
  @obj.site.baseurl
107
147
  end
@@ -111,7 +151,7 @@ class RageRender::WebcomicDrop < Jekyll::Drops::Drop
111
151
  end
112
152
 
113
153
  def copyrights
114
- @obj.site.config['copyrights'].gsub('[year]', Date.today.year.to_s)
154
+ escape @obj.site.config['copyrights'].gsub('[year]', Date.today.year.to_s)
115
155
  end
116
156
 
117
157
  def banner
@@ -122,6 +162,11 @@ class RageRender::WebcomicDrop < Jekyll::Drops::Drop
122
162
  Pathname.new(@obj.site.baseurl || '/').join(@obj.site.config['webcomicavatar'] || '').to_path
123
163
  end
124
164
 
165
+ def webcomicicon
166
+ @obj.site.config.fetch('webcomicavatar', '')
167
+ end
168
+ def_image_metadata :webcomicicon
169
+
125
170
  def hasblogs
126
171
  @obj.site.posts.docs.any?
127
172
  end
@@ -132,7 +177,7 @@ class RageRender::WebcomicDrop < Jekyll::Drops::Drop
132
177
 
133
178
  def extrapages
134
179
  @obj.site.pages.reject {|page| page.data['hidden'] }.map do |page|
135
- {'link' => page.url, 'title' => page.data['title']}
180
+ {'link' => page.url, 'title' => escape(page.data['title'])}
136
181
  end
137
182
  end
138
183
 
@@ -150,8 +195,17 @@ class RageRender::WebcomicDrop < Jekyll::Drops::Drop
150
195
  css_files.map {|f| File.read f }.join
151
196
  end
152
197
 
153
- delegate_method_as :url, :permalink
154
- def_data_delegator :title, :pagetitle
198
+ def layoutcss
199
+ <<~HTML
200
+ <style type="text/css">
201
+ #{css}
202
+ </style>
203
+ HTML
204
+ end
205
+
206
+ def pagetitle
207
+ escape @obj.data['title']
208
+ end
155
209
 
156
210
  def iscomicpage
157
211
  @obj.type == :comics
@@ -45,13 +45,13 @@ module RageRender
45
45
  /!?/.r.map {|c| c == '!' },
46
46
  PATH.map {|p| Variable.new(p) },
47
47
  optional(OPERATOR),
48
- optional(VARIABLE | TEXT))
48
+ optional(VARIABLE | /[^\]]+/.r))
49
49
  ).map {|(reversed, lhs, operator, rhs)| Conditional.new reversed, lhs, operator, rhs }
50
50
 
51
51
  # FUNCTION with no arguments: 'f:cowsay' => Function.new("cowsay", [])
52
52
  # FUNCTION with a variable argument: 'f:js|v:foo' => Function.new('js', [Variable.new(['foo'])])
53
53
  # FUNCTION with literal arguments: 'f:add|2|3' => Function.new('add', ['2', '3'])
54
- FUNCTION = ('f:'.r >> seq(IDENT, ('|'.r >> (VARIABLE | TEXT)).star)).map {|(name, params)| Function.new name, params }
54
+ FUNCTION = ('f:'.r >> seq(IDENT, ('|'.r >> (VARIABLE | /[^\]\|]+/.r)).star)).map {|(name, params)| Function.new name, params }
55
55
 
56
56
  # TAG matches variable tags: '[v:value]' => Variable.new(["value"])
57
57
  # TAG matches loop tags: '[l:loop]' => Loop.new(["loop"])
@@ -20,7 +20,7 @@ module RageRender
20
20
  elsif chunk.path == ['l', 'iteration']
21
21
  '<%= index %>'
22
22
  else
23
- "<%= #{chunk.path.join('.')}.to_s.gsub(/'/, '&#039;').gsub(/\"/, '&quot;') rescue nil %>"
23
+ "<%= #{chunk.path.join('.')} rescue nil %>"
24
24
  end
25
25
 
26
26
  when Language::Conditional
@@ -70,23 +70,13 @@ module RageRender
70
70
  when /^[0-9]+$/
71
71
  param
72
72
  else
73
- "\"#{param}\""
73
+ "\"#{param.gsub(/['"]/) {|c| "\\" + c}}\""
74
74
  end
75
75
  end
76
76
 
77
77
  if ERB_OPERATORS.include? chunk.name
78
78
  "<%= #{params.join(ERB_OPERATORS[chunk.name])} %>"
79
- elsif chunk.name == 'rawhtml'
80
- "<%= #{params.first} %>"
81
79
  else
82
- params = params.zip(chunk.params).map do |param, old_param|
83
- case old_param
84
- when Language::Variable
85
- param + ".to_s.gsub(/'/, '&#039;').gsub(/\"/, '&quot;')"
86
- else
87
- param
88
- end
89
- end
90
80
  "<%= #{chunk.name}(#{params.join(', ')}) %>"
91
81
  end
92
82
 
@@ -1,3 +1,4 @@
1
+ require 'digest'
1
2
  require_relative 'language'
2
3
 
3
4
  module RageRender
@@ -6,37 +7,20 @@ module RageRender
6
7
  'subtract' => proc {|f| [Language::Function.new('minus', f.params)] },
7
8
  'multiply' => proc {|f| [Language::Function.new('times', f.params)] },
8
9
  'divide' => proc {|f| [Language::Function.new('divided_by', f.params)] },
9
- 'removehtmltags' => proc {|f| QUOTE_ESCAPER.call(Language::Function.new('strip_html', f.params)) },
10
- 'rawhtml' => proc {|f| f.params }
10
+ 'removehtmltags' => proc {|f| Language::Function.new('strip_html', f.params) },
11
11
  }
12
12
 
13
- QUOTE_REPLACEMENTS = {
14
- '"' => '&quot;',
15
- "'" => '&#039;',
16
- }
17
-
18
- REPLACE_QUOTES = ["replace: '\"', '&quot;'", "replace: \"'\", '&#039;'"].join(' | ')
19
-
20
- QUOTE_ESCAPER = proc do |f|
21
- output = []
22
- params = f.params.each_with_index do |param, index|
23
- case param
24
- when Language::Variable
25
- new_name = param.path.join('_')
26
- output << "{% assign #{new_name} = #{render_value(param)} | #{REPLACE_QUOTES} %}"
27
- Language::Variable.new([new_name])
28
- else
29
- param
30
- end
31
- end
32
- output << Language::Function.new(f.name, params)
33
- output
34
- end
35
-
36
- def self.render_value value
13
+ def self.render_value value, literals
37
14
  case value
38
15
  when String
39
- value =~ /^[0-9]+$/ ? value : "\"#{value}\""
16
+ case value
17
+ when /^[0-9]+$/
18
+ value
19
+ when /"'/
20
+ literals[value]
21
+ else
22
+ "\"#{value}\""
23
+ end
40
24
  when Language::Variable
41
25
  if value.path.first == 'l' && value.path.last == 'iteration'
42
26
  'forloop.index0'
@@ -46,7 +30,7 @@ module RageRender
46
30
  value.path.join('.')
47
31
  end
48
32
  when Language::Function
49
- params = value.params.map {|p| render_value p }
33
+ params = value.params.map {|p| render_value p, literals }
50
34
  args = params.drop(1).map {|p| "#{value.name}: #{p}" }.join(' | ')
51
35
  [params.first, args.empty? ? value.name : args].join(' | ')
52
36
  when nil
@@ -56,20 +40,21 @@ module RageRender
56
40
 
57
41
  def self.to_liquid document
58
42
  tag_stack = Array.new
43
+ literals = Hash.new {|cache, l| cache[l] = "str" + Digest::MD5.hexdigest(l)}
59
44
 
60
- document.map do |chunk|
45
+ chunks = document.map do |chunk|
61
46
  case chunk
62
47
  when String
63
48
  chunk
64
49
 
65
50
  when Language::Variable
66
- "{{ #{render_value chunk} | #{REPLACE_QUOTES} }}"
51
+ "{{ #{render_value chunk, literals} }}"
67
52
 
68
53
  when Language::Conditional
69
54
  tag_stack << (chunk.reversed ? :endunless : :endif)
70
55
 
71
- lhs = render_value chunk.lhs
72
- rhs = render_value chunk.rhs
56
+ lhs = render_value chunk.lhs, literals
57
+ rhs = render_value chunk.rhs, literals
73
58
  operator = chunk.operator
74
59
 
75
60
  if chunk.lhs.is_a?(Language::Variable) && chunk.lhs.path.first == "l"
@@ -105,8 +90,8 @@ module RageRender
105
90
  output.join
106
91
 
107
92
  when Language::Function
108
- *output, func = LIQUID_FUNCTIONS.fetch(chunk.name, QUOTE_ESCAPER).call(chunk)
109
- output << "{{ #{render_value(func)} }}"
93
+ *output, func = LIQUID_FUNCTIONS.fetch(chunk.name, proc{|f| f}).call(chunk)
94
+ output << "{{ #{render_value(func, literals)} }}"
110
95
  output.join
111
96
 
112
97
  when Language::Loop
@@ -124,6 +109,10 @@ module RageRender
124
109
  end
125
110
  end
126
111
  end
112
+
113
+ literals.map do |(value, name)|
114
+ "{% capture #{name} %}#{value}{% endcapture %}"
115
+ end + chunks
127
116
  end
128
117
  end
129
118
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ragerender
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.4
4
+ version: 0.1.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Simon Worthington
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-07-20 00:00:00.000000000 Z
11
+ date: 2025-09-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rsec
@@ -38,6 +38,20 @@ dependencies:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
40
  version: '1.3'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rexml
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3'
41
55
  - !ruby/object:Gem::Dependency
42
56
  name: rake
43
57
  requirement: !ruby/object:Gem::Requirement
@@ -138,10 +152,14 @@ description: |-
138
152
  you added your first comic!
139
153
 
140
154
  If you want to add an author note, create a text file in a folder called
141
- <tt>_comics</tt> that has the same file name, but with a <tt>.md</tt> extension:
155
+ <tt>_comics</tt> that has the same file name, but with a <tt>.txt</tt> extension:
142
156
 
143
157
  mkdir _comics
144
- echo "Check out my cool comic y'all!" > '_comics/My first page.md'
158
+ echo "Check out my cool comic y'all!" > '_comics/My first page.txt'
159
+
160
+ Or use HTML:
161
+
162
+ echo "This is my <strong>first</strong> page!" > '_comics/My first page.html'
145
163
 
146
164
  Generate the site using:
147
165
 
@@ -181,8 +199,15 @@ description: |-
181
199
 
182
200
  If you want to use your own layout code, then create a <tt>_layouts</tt>
183
201
  directory and put the contents of each of your ComicFury layout tabs in there,
184
- and then put your CSS in the main folder. You should end up with a full set of
185
- files like:
202
+ and then put your CSS in the main folder.
203
+
204
+ The easiest way is to go to your Webcomic Management, click "Edit Layout", then
205
+ in the box labelled "Useful", click "Download Layout Backup". Pass this file to
206
+ RageRender, which will <tt>unpack</tt> it for you:
207
+
208
+ bundle exec jekyll unpack mycomic-2025-09-13.cflxml
209
+
210
+ You should end up with a full set of files like:
186
211
 
187
212
  _layouts
188
213
  archive.html
@@ -200,7 +225,7 @@ description: |-
200
225
 
201
226
  === Adding blogs
202
227
 
203
- Add your blogs into a folder called `_posts`:
228
+ Add your blogs into a folder called <tt>_posts</tt>:
204
229
 
205
230
  cat _posts/2025-05-29-my-new-comic.md
206
231
  Hey guys, welcome to my new comic! It's gonna be so sick!
@@ -267,6 +292,16 @@ description: |-
267
292
  - anything else will display the extra page that has the matching
268
293
  <tt>slug</tt> in its Front Matter
269
294
 
295
+ === Putting changes on ComicFury
296
+
297
+ Once you're done making changes, you can <tt>pack</tt> your layout:
298
+
299
+ bundle exec jekyll pack
300
+
301
+ The resulting file can be uploaded to ComicFury by going to your Webcomic
302
+ Management, clicking "Edit Layout", then in the box labelled "Useful", click
303
+ "Restore Layout Backup".
304
+
270
305
  === Stuff that doesn't work
271
306
 
272
307
  Here is a probably incomplete list of things you can expect to be different
@@ -318,6 +353,7 @@ files:
318
353
  - assets/overview.html
319
354
  - assets/search.html
320
355
  - lib/ragerender.rb
356
+ - lib/ragerender/cflxml.rb
321
357
  - lib/ragerender/date_formats.rb
322
358
  - lib/ragerender/functions.rb
323
359
  - lib/ragerender/jekyll.rb
@@ -327,9 +363,9 @@ files:
327
363
  - lib/ragerender/jekyll/chapter.rb
328
364
  - lib/ragerender/jekyll/comics.rb
329
365
  - lib/ragerender/jekyll/error.rb
330
- - lib/ragerender/jekyll/named_data_delegator.rb
331
366
  - lib/ragerender/jekyll/overview.rb
332
367
  - lib/ragerender/jekyll/pagination.rb
368
+ - lib/ragerender/jekyll/pipettes.rb
333
369
  - lib/ragerender/jekyll/search.rb
334
370
  - lib/ragerender/jekyll/setup_collection.rb
335
371
  - lib/ragerender/language.rb
@@ -1,9 +0,0 @@
1
- module RageRender
2
- module NamedDataDelegator
3
- def def_data_delegator key, aliaz
4
- define_method(aliaz.to_sym) do
5
- @obj.data[key.to_s]
6
- end
7
- end
8
- end
9
- end