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.
- checksums.yaml +4 -4
- data/README.md +73 -7
- data/lib/jektex/cache.rb +107 -0
- data/lib/jektex/configuration.rb +167 -0
- data/lib/jektex/hooks.rb +51 -0
- data/lib/jektex/katex.min.js +1 -1
- data/lib/jektex/page_processor.rb +304 -0
- data/lib/jektex/renderer.rb +81 -0
- data/lib/jektex/reporter.rb +70 -0
- data/lib/jektex/version.rb +1 -1
- data/lib/jektex.rb +9 -4
- metadata +46 -23
- data/lib/jektex/jektex.rb +0 -215
|
@@ -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
|
data/lib/jektex/version.rb
CHANGED
data/lib/jektex.rb
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
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
|
-
|
|
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.
|
|
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:
|
|
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.
|
|
18
|
+
version: '2.9'
|
|
20
19
|
- - ">="
|
|
21
20
|
- !ruby/object:Gem::Version
|
|
22
|
-
version: 2.
|
|
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.
|
|
28
|
+
version: '2.9'
|
|
30
29
|
- - ">="
|
|
31
30
|
- !ruby/object:Gem::Version
|
|
32
|
-
version: 2.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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/
|
|
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:
|
|
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:
|
|
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: []
|