jektex 0.1.0 → 0.2.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,304 @@
1
+ require 'digest'
2
+ require 'execjs'
3
+ require 'htmlentities'
4
+
5
+ module Jektex
6
+ # Finds math expressions in a page, decides whether the page should be
7
+ # processed at all, and answers each expression from the cache or by
8
+ # rendering it. This is the orchestration layer between Jekyll hooks
9
+ # and the renderer/cache.
10
+ #
11
+ # Rendering happens in two phases. At pre_render every LaTeX-notation
12
+ # expression in the raw content is swapped for an inert token, so Liquid
13
+ # and kramdown treat math like ordinary text and cannot mangle it. At
14
+ # post_render the final HTML structure is known, so tokens inside code
15
+ # markup turn back into their original source text (a code sample about
16
+ # LaTeX must show the LaTeX, see github issue #5) while all other math
17
+ # is rendered.
18
+ #
19
+ # Kramdown notation is converted to LaTeX notation by kramdown itself —
20
+ # except inside raw HTML blocks, which kramdown does not process; the
21
+ # $$..$$ expressions left there are found and rendered too (issue #7),
22
+ # but only for markdown source files, where $$ has a defined meaning.
23
+ #
24
+ # All expressions a page needs are collected first and rendered in one
25
+ # batched KaTeX call, because each call to an external ExecJS runtime
26
+ # costs far more than the rendering itself.
27
+ class PageProcessor
28
+ # The (?<!\\) lookbehind skips escaped delimiters. It is also what
29
+ # prevents the post_render pass from re-matching \\[..] sequences
30
+ # inside the TeX source that KaTeX embeds in its own output.
31
+ INLINE_MATH = /(\\\()(.*?)(?<!\\)\\\)/m
32
+ DISPLAY_MATH = /(\\\[)(.*?)(?<!\\)\\\]/m
33
+
34
+ TOKEN = /jektexprotected[0-9a-f]{32}/
35
+
36
+ # everything resolve_math reacts to, found in one pass
37
+ MATH_PATTERN = /(?<inline>\\\((?<inline_body>.*?)(?<!\\)\\\))|(?<display>\\\[(?<display_body>.*?)(?<!\\)\\\])|(?<token>#{TOKEN.source})/m
38
+
39
+ # kramdown converts its $$..$$ notation everywhere except inside raw
40
+ # HTML blocks; these leftovers in the output of markdown pages are
41
+ # rendered too (github issue #7). (?<!\\) honors \$$ as an opt-out
42
+ # and keeps \$ inside a body from closing the expression.
43
+ DOLLAR_MATH = /(?<dollars>(?<!\\)\$\$(?<dollars_body>.+?)(?<!\\)\$\$)/m
44
+
45
+ # never used on protected segments: the dollars alternative could
46
+ # swallow a protection token between two dollar signs there
47
+ MATH_PATTERN_WITH_DOLLARS = /#{MATH_PATTERN.source}|#{DOLLAR_MATH.source}/m
48
+
49
+ # markup whose contents must never be rendered
50
+ # (the same set KaTeX's own auto-render extension ignores)
51
+ PROTECTED_ELEMENTS = %r{<(pre|code|script|style|textarea)\b[^>]*>.*?</\1\s*>}mi
52
+
53
+ ProtectedExpression = Struct.new(:source, :body, :display_mode)
54
+
55
+ # a single substitution: replace text[position, length] with either the
56
+ # rendering of expression body (kind :expression, source is the fallback
57
+ # when rendering is impossible) or a literal string (kind :text)
58
+ Instruction = Struct.new(:position, :length, :kind, :body, :display_mode, :source)
59
+
60
+ attr_reader :rendered_count, :cache_hit_count
61
+
62
+ def initialize(config:, cache:, renderer:, reporter:)
63
+ @config = config
64
+ @cache = cache
65
+ @renderer = renderer
66
+ @reporter = reporter
67
+ @entity_parser = HTMLEntities.new
68
+ # tokens must stay resolvable across watch-mode rebuilds, because other
69
+ # pages can embed token-carrying content of pages that are not rebuilt
70
+ @protected_math = Hash.new
71
+ # batch results waiting for their first substitution: [expression, mode] => html
72
+ @freshly_rendered = Hash.new
73
+ # failed expressions of the current page: [expression, mode] => fallback html or nil
74
+ @error_fallbacks = Hash.new
75
+ reset_counters
76
+ end
77
+
78
+ # pre_render: protect LaTeX-notation math in the raw content
79
+ def process_content(page)
80
+ return page.content if !page.data || ignored?(page)
81
+ text = page.content.to_s
82
+ return text unless text.include?('\(') || text.include?('\[')
83
+ return protect_math(text)
84
+ end
85
+
86
+ # post_render: render the protected tokens and the kramdown-notation
87
+ # math (kramdown turns its $$..$$ into \(..\)/\[..\] during conversion,
88
+ # except inside raw HTML blocks, where the $$..$$ survives verbatim)
89
+ def process_output(page)
90
+ return restore_protected_math(page.output) if !page.data || ignored?(page)
91
+ return resolve_math(page.output.to_s, page.relative_path,
92
+ dollars: markdown_page?(page))
93
+ end
94
+
95
+ def protect_math(text)
96
+ text = text.gsub(INLINE_MATH) { protect_expression($~[0], $2, false) }
97
+ return text.gsub(DISPLAY_MATH) { protect_expression($~[0], $2, true) }
98
+ end
99
+
100
+ def resolve_math(text, doc_path, dollars: false)
101
+ return text unless contains_math?(text, dollars)
102
+ instructions = collect_instructions(text, dollars)
103
+ render_missing_expressions(instructions, doc_path)
104
+ return apply_instructions(text, instructions, doc_path)
105
+ end
106
+
107
+ # immediate rendering in a plain string, without code protection
108
+ def render_math(text, doc_path)
109
+ instructions = Array.new
110
+ scan_segment(text, 0, :plain, instructions)
111
+ render_missing_expressions(instructions, doc_path)
112
+ return apply_instructions(text, instructions, doc_path)
113
+ end
114
+
115
+ def ignored?(page)
116
+ front_matter_flag = page.data[@config.front_matter_tag]
117
+ return true if front_matter_flag == false || front_matter_flag == "false"
118
+ return @config.ignore.any? do |pattern|
119
+ File.fnmatch?(pattern, page.relative_path, File::FNM_DOTMATCH)
120
+ end
121
+ end
122
+
123
+ def reset_counters
124
+ @rendered_count = 0
125
+ @cache_hit_count = 0
126
+ end
127
+
128
+ private
129
+
130
+ # An ignored page must not render math, but it can still carry
131
+ # protection tokens — feeds embed the content of processed posts.
132
+ # Those tokens must never leak into the published site, so they
133
+ # are turned back into their original source text.
134
+ def restore_protected_math(output)
135
+ return output unless output.is_a?(String) && output.include?("jektexprotected")
136
+ return output.gsub(TOKEN) do |token|
137
+ expression = @protected_math[token]
138
+ expression ? expression.source : token
139
+ end
140
+ end
141
+
142
+ def contains_math?(text, dollars)
143
+ return true if text.include?('\(') || text.include?('\[') || text.include?("jektexprotected")
144
+ return dollars && text.include?("$$")
145
+ end
146
+
147
+ def markdown_page?(page)
148
+ return @config.markdown_extensions.include?(File.extname(page.relative_path.to_s).downcase)
149
+ end
150
+
151
+ def protect_expression(source, body, display_mode)
152
+ token = "jektexprotected" + Digest::SHA2.hexdigest(source)[0, 32]
153
+ @protected_math[token] = ProtectedExpression.new(source, body, display_mode)
154
+ return token
155
+ end
156
+
157
+ ##### scanning
158
+
159
+ def collect_instructions(text, dollars)
160
+ instructions = Array.new
161
+ position = 0
162
+ text.scan(PROTECTED_ELEMENTS) do
163
+ match = Regexp.last_match
164
+ scan_segment(text[position...match.begin(0)], position, :plain, instructions,
165
+ text: text, dollars: dollars)
166
+ scan_segment(match[0], match.begin(0), :protected, instructions)
167
+ position = match.end(0)
168
+ end
169
+ scan_segment(text[position..].to_s, position, :plain, instructions,
170
+ text: text, dollars: dollars)
171
+ return instructions
172
+ end
173
+
174
+ def scan_segment(segment, offset, kind, instructions, text: segment, dollars: false)
175
+ pattern = dollars ? MATH_PATTERN_WITH_DOLLARS : MATH_PATTERN
176
+ segment.scan(pattern) do
177
+ match = Regexp.last_match
178
+ position = offset + match.begin(0)
179
+ if match[:token]
180
+ expression = @protected_math[match[0]]
181
+ next unless expression
182
+ if kind == :protected
183
+ # a code sample must show the LaTeX source, not the rendering
184
+ instructions.append(Instruction.new(position, match[0].length, :text,
185
+ nil, nil, expression.source))
186
+ else
187
+ instructions.append(Instruction.new(position, match[0].length, :expression,
188
+ expression.body, expression.display_mode,
189
+ expression.source))
190
+ end
191
+ elsif kind == :plain
192
+ # the dollars guard is required: asking a plain MATH_PATTERN
193
+ # match for the group raises IndexError
194
+ if dollars && match[:dollars]
195
+ instructions.append(Instruction.new(position, match[0].length, :expression,
196
+ match[:dollars_body],
197
+ standalone_line?(text, position, position + match[0].length),
198
+ match[0]))
199
+ else
200
+ body = match[:inline_body] || match[:display_body]
201
+ instructions.append(Instruction.new(position, match[0].length, :expression,
202
+ body, !match[:display].nil?, match[0]))
203
+ end
204
+ end
205
+ end
206
+ end
207
+
208
+ # kramdown renders $$..$$ standing alone on its line(s) as display math
209
+ # and mid-text occurrences as inline math; mirror that on the output text
210
+ def standalone_line?(text, start, stop)
211
+ line_start = start.zero? ? 0 : ((text.rindex("\n", start - 1) || -1) + 1)
212
+ line_end = text.index("\n", stop) || text.length
213
+ return text[line_start...start].match?(/\A\s*\z/) &&
214
+ text[stop...line_end].match?(/\A\s*\z/)
215
+ end
216
+
217
+ ##### rendering
218
+
219
+ # renders everything the instructions need but the cache cannot answer,
220
+ # in a single call to the JavaScript runtime
221
+ def render_missing_expressions(instructions, doc_path)
222
+ @error_fallbacks = Hash.new
223
+ needed = instructions.filter_map do |instruction|
224
+ next unless instruction.kind == :expression
225
+ [@entity_parser.decode(instruction.body), instruction.display_mode]
226
+ end.uniq
227
+ missing = needed.reject { |expression, mode| @cache.fetch(expression, mode) }
228
+ return if missing.empty?
229
+
230
+ begin
231
+ results = @renderer.render_batch(missing)
232
+ rescue SystemExit, Interrupt
233
+ # save the work done so far, then let Jekyll die properly
234
+ @cache.save
235
+ raise
236
+ end
237
+
238
+ missing.zip(results) do |(expression, mode), result|
239
+ if result.error
240
+ @reporter.error(result.error, doc_path)
241
+ @error_fallbacks[[expression, mode]] = result.html
242
+ else
243
+ @cache.store(expression, mode, result.html)
244
+ @freshly_rendered[[expression, mode]] = result.html
245
+ end
246
+ end
247
+ end
248
+
249
+ def apply_instructions(text, instructions, doc_path)
250
+ result = +""
251
+ position = 0
252
+ instructions.each do |instruction|
253
+ result << text[position...instruction.position]
254
+ result << if instruction.kind == :text
255
+ instruction.source
256
+ else
257
+ render_expression(instruction.body,
258
+ display_mode: instruction.display_mode,
259
+ doc_path: doc_path) || instruction.source
260
+ end
261
+ position = instruction.position + instruction.length
262
+ end
263
+ result << text[position..].to_s
264
+ return result
265
+ end
266
+
267
+ # Returns nil only for expressions that failed so hard that not even
268
+ # error highlighting could be rendered — callers keep the source text.
269
+ def render_expression(expression, display_mode:, doc_path:)
270
+ expression = @entity_parser.decode(expression)
271
+ key = [expression, display_mode]
272
+
273
+ return @error_fallbacks[key] if @error_fallbacks.key?(key)
274
+
275
+ if (batched_html = @freshly_rendered.delete(key))
276
+ @rendered_count += 1
277
+ @reporter.progress(@rendered_count, @cache_hit_count)
278
+ return batched_html
279
+ end
280
+
281
+ if (cached_html = @cache.fetch(expression, display_mode))
282
+ @cache_hit_count += 1
283
+ @reporter.progress(@rendered_count, @cache_hit_count)
284
+ return cached_html
285
+ end
286
+
287
+ # not covered by the batch — render it individually
288
+ begin
289
+ html = @renderer.render(expression, display_mode: display_mode)
290
+ rescue SystemExit, Interrupt
291
+ @cache.save
292
+ raise
293
+ rescue ExecJS::ProgramError => error
294
+ @reporter.error(error.message, doc_path)
295
+ return @renderer.render_with_error_fallback(expression, display_mode: display_mode)
296
+ end
297
+
298
+ @cache.store(expression, display_mode, html)
299
+ @rendered_count += 1
300
+ @reporter.progress(@rendered_count, @cache_hit_count)
301
+ return html
302
+ end
303
+ end
304
+ end
@@ -0,0 +1,81 @@
1
+ require 'execjs'
2
+
3
+ module Jektex
4
+ # Wraps the bundled KaTeX JavaScript. Knows nothing about pages,
5
+ # caching or Jekyll — it turns expressions into HTML.
6
+ class Renderer
7
+ # With an external ExecJS runtime (node, bun) every call spawns a fresh
8
+ # process that has to parse the whole KaTeX bundle (~70 ms), while the
9
+ # rendering itself takes single milliseconds. Rendering a whole batch
10
+ # of expressions in one call amortizes that overhead.
11
+ BATCH_HELPER = <<~JS
12
+ function jektexRenderBatch(items) {
13
+ return items.map(function (item) {
14
+ var expression = item[0];
15
+ var options = item[1];
16
+ try {
17
+ return { html: katex.renderToString(expression, options) };
18
+ } catch (error) {
19
+ var message = String(error.message || error);
20
+ options.throwOnError = false;
21
+ try {
22
+ return { error: message, html: katex.renderToString(expression, options) };
23
+ } catch (unrenderableError) {
24
+ return { error: message, html: null };
25
+ }
26
+ }
27
+ });
28
+ }
29
+ JS
30
+
31
+ # error is nil for successful renders; html is the rendered expression,
32
+ # for failed ones rendered with the error highlighted in place — or nil
33
+ # when even that was impossible
34
+ RenderedExpression = Struct.new(:html, :error)
35
+
36
+ def initialize(config)
37
+ @path_to_katex_js = config.path_to_katex_js
38
+ @global_macros = config.global_macros
39
+ @katex_options = config.katex_options
40
+ end
41
+
42
+ # Renders [[expression, display_mode], ...] in one JavaScript call.
43
+ # Returns a RenderedExpression for every item, in the same order.
44
+ # A parse error in one expression does not affect the others.
45
+ def render_batch(expressions)
46
+ return [] if expressions.empty?
47
+ items = expressions.map do |expression, display_mode|
48
+ [expression, base_options(display_mode)]
49
+ end
50
+ results = katex.call("jektexRenderBatch", items)
51
+ return results.map { |result| RenderedExpression.new(result["html"], result["error"]) }
52
+ end
53
+
54
+ # Raises ExecJS::ProgramError when the expression is invalid LaTeX.
55
+ def render(expression, display_mode:)
56
+ katex.call("katex.renderToString", expression, base_options(display_mode))
57
+ end
58
+
59
+ # Never raises on invalid LaTeX; KaTeX renders the error into the
60
+ # document instead (red highlighting).
61
+ def render_with_error_fallback(expression, display_mode:)
62
+ katex.call("katex.renderToString", expression,
63
+ base_options(display_mode).merge(throwOnError: false))
64
+ end
65
+
66
+ private
67
+
68
+ # compiling the KaTeX bundle takes ~100 ms, so defer it to the first
69
+ # render instead of paying for it at plugin load time
70
+ def katex
71
+ @katex ||= ExecJS.compile(File.read(@path_to_katex_js) + BATCH_HELPER)
72
+ end
73
+
74
+ # user options first, so the keys jektex decides itself always win
75
+ # (reserved keys are already stripped by the config, this is a backstop)
76
+ def base_options(display_mode)
77
+ @katex_options.merge(displayMode: display_mode,
78
+ macros: @global_macros)
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,70 @@
1
+ module Jektex
2
+ # All console output of the plugin. Every method is a no-op when the
3
+ # silent option is set, so callers never have to check it themselves.
4
+ class Reporter
5
+ def initialize(config, out: $stdout, err: $stderr)
6
+ @out = out
7
+ @err = err
8
+ @silent = config.silent
9
+ @indent = config.console_indent
10
+ @last_progress_time = nil
11
+ end
12
+
13
+ # Configuration problems must stay visible even with the silent
14
+ # option set — a broken config could be the reason silence was
15
+ # requested by accident. They go to stderr, so redirecting the
16
+ # build output does not hide them either.
17
+ def warn(message)
18
+ @err.puts "\e[33m#{@indent}JekTeX: #{message}\e[0m"
19
+ end
20
+
21
+ def info(message)
22
+ return if @silent
23
+ @out.puts "#{@indent}LaTeX: #{message}"
24
+ end
25
+
26
+ def macro_summary(macro_count, updated_count)
27
+ return if @silent
28
+ if macro_count == 0
29
+ info("no macros loaded")
30
+ else
31
+ message = "#{macro_count} macro#{macro_count == 1 ? "" : "s"} loaded"
32
+ message += " (#{updated_count} updated)" if updated_count > 0
33
+ info(message)
34
+ end
35
+ end
36
+
37
+ def error(message, doc_path)
38
+ return if @silent
39
+ @out.puts "\e[31m #{message.gsub("ParseError: ", "")}\n\t#{doc_path}\e[0m"
40
+ end
41
+
42
+ # more frequent updates than this are invisible anyway, and printing
43
+ # one per expression measurably slows down large cached builds
44
+ PROGRESS_UPDATE_INTERVAL = 0.1
45
+
46
+ def progress(rendered, from_cache)
47
+ return if @silent
48
+ now = Time.now
49
+ return if @last_progress_time && now - @last_progress_time < PROGRESS_UPDATE_INTERVAL
50
+ @last_progress_time = now
51
+ print_progress_line(rendered, from_cache)
52
+ end
53
+
54
+ def finish(rendered, from_cache)
55
+ return if @silent
56
+ print_progress_line(rendered, from_cache)
57
+ @out.print "\n"
58
+ end
59
+
60
+ private
61
+
62
+ # single line overwritten in place with a carriage return;
63
+ # padded to a fixed width so a shorter update fully covers the previous one
64
+ def print_progress_line(rendered, from_cache)
65
+ line = "#{@indent}LaTeX: #{rendered} expressions rendered (#{from_cache} loaded from cache)"
66
+ @out.print line.ljust(72) + "\r"
67
+ @out.flush
68
+ end
69
+ end
70
+ end
@@ -1,3 +1,3 @@
1
1
  module Jektex
2
- VERSION = "0.1.0"
2
+ VERSION = "0.2.0"
3
3
  end
data/lib/jektex.rb CHANGED
@@ -1,5 +1,10 @@
1
- module Jektex
2
- autoload :VERSION, "jektex/version.rb"
3
- end
1
+ require "jektex/version"
2
+ require "jektex/configuration"
3
+ require "jektex/reporter"
4
+ require "jektex/renderer"
5
+ require "jektex/cache"
6
+ require "jektex/page_processor"
4
7
 
5
- require "jektex/jektex.rb"
8
+ # when loaded as a Jekyll plugin, Jekyll is always defined; the guard
9
+ # lets the gem be required standalone (for tests and other tooling)
10
+ require "jektex/hooks" if defined?(Jekyll)
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jektex
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jan Černý
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2022-11-28 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: execjs
@@ -16,40 +15,34 @@ dependencies:
16
15
  requirements:
17
16
  - - "~>"
18
17
  - !ruby/object:Gem::Version
19
- version: '2.8'
18
+ version: '2.9'
20
19
  - - ">="
21
20
  - !ruby/object:Gem::Version
22
- version: 2.8.1
21
+ version: 2.9.1
23
22
  type: :runtime
24
23
  prerelease: false
25
24
  version_requirements: !ruby/object:Gem::Requirement
26
25
  requirements:
27
26
  - - "~>"
28
27
  - !ruby/object:Gem::Version
29
- version: '2.8'
28
+ version: '2.9'
30
29
  - - ">="
31
30
  - !ruby/object:Gem::Version
32
- version: 2.8.1
31
+ version: 2.9.1
33
32
  - !ruby/object:Gem::Dependency
34
33
  name: digest
35
34
  requirement: !ruby/object:Gem::Requirement
36
35
  requirements:
37
- - - "~>"
38
- - !ruby/object:Gem::Version
39
- version: '3.0'
40
36
  - - ">="
41
37
  - !ruby/object:Gem::Version
42
- version: 3.0.0
38
+ version: 3.1.1
43
39
  type: :runtime
44
40
  prerelease: false
45
41
  version_requirements: !ruby/object:Gem::Requirement
46
42
  requirements:
47
- - - "~>"
48
- - !ruby/object:Gem::Version
49
- version: '3.0'
50
43
  - - ">="
51
44
  - !ruby/object:Gem::Version
52
- version: 3.0.0
45
+ version: 3.1.1
53
46
  - !ruby/object:Gem::Dependency
54
47
  name: htmlentities
55
48
  requirement: !ruby/object:Gem::Requirement
@@ -76,7 +69,7 @@ dependencies:
76
69
  requirements:
77
70
  - - "~>"
78
71
  - !ruby/object:Gem::Version
79
- version: '2.0'
72
+ version: '2.6'
80
73
  - - ">="
81
74
  - !ruby/object:Gem::Version
82
75
  version: 2.0.0
@@ -86,10 +79,38 @@ dependencies:
86
79
  requirements:
87
80
  - - "~>"
88
81
  - !ruby/object:Gem::Version
89
- version: '2.0'
82
+ version: '2.6'
90
83
  - - ">="
91
84
  - !ruby/object:Gem::Version
92
85
  version: 2.0.0
86
+ - !ruby/object:Gem::Dependency
87
+ name: rake
88
+ requirement: !ruby/object:Gem::Requirement
89
+ requirements:
90
+ - - "~>"
91
+ - !ruby/object:Gem::Version
92
+ version: '13.0'
93
+ type: :development
94
+ prerelease: false
95
+ version_requirements: !ruby/object:Gem::Requirement
96
+ requirements:
97
+ - - "~>"
98
+ - !ruby/object:Gem::Version
99
+ version: '13.0'
100
+ - !ruby/object:Gem::Dependency
101
+ name: test-unit
102
+ requirement: !ruby/object:Gem::Requirement
103
+ requirements:
104
+ - - "~>"
105
+ - !ruby/object:Gem::Version
106
+ version: '3.6'
107
+ type: :development
108
+ prerelease: false
109
+ version_requirements: !ruby/object:Gem::Requirement
110
+ requirements:
111
+ - - "~>"
112
+ - !ruby/object:Gem::Version
113
+ version: '3.6'
93
114
  description: Highly optimized and cached latex server side rendering for Jekyll with
94
115
  macros and dynamic output
95
116
  email: jc@ucw.cz
@@ -100,8 +121,13 @@ files:
100
121
  - LICENSE
101
122
  - README.md
102
123
  - lib/jektex.rb
103
- - lib/jektex/jektex.rb
124
+ - lib/jektex/cache.rb
125
+ - lib/jektex/configuration.rb
126
+ - lib/jektex/hooks.rb
104
127
  - lib/jektex/katex.min.js
128
+ - lib/jektex/page_processor.rb
129
+ - lib/jektex/renderer.rb
130
+ - lib/jektex/reporter.rb
105
131
  - lib/jektex/version.rb
106
132
  homepage: https://github.com/yagarea/jektex
107
133
  licenses:
@@ -109,10 +135,8 @@ licenses:
109
135
  metadata:
110
136
  bug_tracker_uri: https://github.com/yagarea/jektex/issues
111
137
  documentation_uri: https://github.com/yagarea/jektex/blob/master/README.md
112
- homepage_uri: https://github.com/yagarea/jektex
113
138
  source_code_uri: https://github.com/yagarea/jektex
114
139
  changelog_uri: https://github.com/yagarea/jektex/blob/master/changelog.md
115
- post_install_message:
116
140
  rdoc_options: []
117
141
  require_paths:
118
142
  - lib
@@ -120,15 +144,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
120
144
  requirements:
121
145
  - - ">="
122
146
  - !ruby/object:Gem::Version
123
- version: 2.7.0
147
+ version: 3.1.0
124
148
  required_rubygems_version: !ruby/object:Gem::Requirement
125
149
  requirements:
126
150
  - - ">="
127
151
  - !ruby/object:Gem::Version
128
152
  version: '0'
129
153
  requirements: []
130
- rubygems_version: 3.3.25
131
- signing_key:
154
+ rubygems_version: 4.0.9
132
155
  specification_version: 4
133
156
  summary: Highly optimized latex rendering for Jekyll
134
157
  test_files: []