jektex 0.1.1 → 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 +71 -5
- 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 +42 -19
- data/lib/jektex/jektex.rb +0 -215
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 63d33439743b4634d9758d3d466832179f0314a0e45679f414f11e13b04d27e7
|
|
4
|
+
data.tar.gz: 3b76e1ffe31724de5c1ce32df26fad3e4f6ad9fdd9df482b9ee019aa009ce5c2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 036e9f9683e6235763e47f9761dbc0b97e40b853cfa3059cc63acc6c7d994d3dbee2afeec415d0fb660cf97edd3911d3fb1acbfff3ab2a1c01a76296b64145dd
|
|
7
|
+
data.tar.gz: 2de454e9296dad57c9701ae361172442f2ed10e86eb5120e8b7ab4f06f75ba5f0b356ad708c9385bf701988bef3a8c6df5ad77a37293fc94361bdf53df5b657c
|
data/README.md
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
# 
|
|
4
4
|
A Jekyll plugin for blazing-fast server-side cached LaTeX rendering, with support for macros.
|
|
5
5
|
Enjoy the comfort of LaTeX and Markdown without cluttering your site with bloated JavaScript.
|
|
6
|
+
This project is [endorsed by KaTeX.org](https://katex.org/docs/libs#jekyll).
|
|
6
7
|
|
|
7
8
|
## Features
|
|
8
9
|
- Renders LaTeX formulas during Jekyll rendering
|
|
@@ -14,6 +15,8 @@ Enjoy the comfort of LaTeX and Markdown without cluttering your site with bloate
|
|
|
14
15
|
- Is very easy to setup
|
|
15
16
|
- Doesn't interfere with Jekyll workflow and project structure
|
|
16
17
|
- Marks invalid expressions in document, printing its location during rendering
|
|
18
|
+
- Leaves LaTeX inside code blocks and inline code untouched, so you can write about LaTeX
|
|
19
|
+
- Renders formulas inside raw HTML blocks, which kramdown skips
|
|
17
20
|
- Is highly configurable with sensible defaults
|
|
18
21
|
- Makes sure that cache does not contain expression rendered with outdated configuration
|
|
19
22
|
- Supports two major LaTeX notations
|
|
@@ -44,7 +47,25 @@ ea commodo consequat.
|
|
|
44
47
|
_Why Jektex does not use conventional single `$` for inline formulas and double `$$` for
|
|
45
48
|
display mode?
|
|
46
49
|
This is how [kramdown](https://kramdown.gettalong.org/) (Jekyll's markdown parser) works
|
|
47
|
-
so I decided to respect this convention. It makes this plugin more consistent and universal.
|
|
50
|
+
so I decided to respect this convention. It makes this plugin more consistent and universal.
|
|
51
|
+
See [this issue](https://github.com/gettalong/kramdown/issues/762) for more context._
|
|
52
|
+
|
|
53
|
+
**Formulas inside raw HTML blocks**
|
|
54
|
+
Kramdown does not process markdown inside block-level HTML tags, so it leaves formulas
|
|
55
|
+
like `<div>$$\beta$$</div>` unconverted. Jektex finds these leftover `$$` formulas after
|
|
56
|
+
the conversion and renders them itself. A formula standing alone on its line renders
|
|
57
|
+
in display mode, a formula inside text flow renders inline:
|
|
58
|
+
```html
|
|
59
|
+
<div>The inline formula $$e^{i\theta}$$ sits in text flow.
|
|
60
|
+
|
|
61
|
+
$$ \left[ \frac{-\hbar^2}{2\mu}\nabla^2 + V(\mathbf{r},t)\right] \Psi(\mathbf{r},t) $$
|
|
62
|
+
</div>
|
|
63
|
+
```
|
|
64
|
+
This applies only to markdown source files and never inside `pre`, `code`, `script`
|
|
65
|
+
and similar tags. You can prevent rendering of a specific formula by escaping it
|
|
66
|
+
as `\$$` or by putting it in a code span. If you prefer kramdown to process the
|
|
67
|
+
content of an HTML block itself, give the tag the
|
|
68
|
+
[`markdown="1"` attribute](https://kramdown.gettalong.org/syntax.html#html-blocks).
|
|
48
69
|
|
|
49
70
|
|
|
50
71
|
### LaTex math mode notation
|
|
@@ -73,11 +94,17 @@ ea commodo consequat.
|
|
|
73
94
|
There is a build in macro for jektex logo. You can use it as `\jektex`.
|
|
74
95
|
|
|
75
96
|
### Config
|
|
76
|
-
Jektex
|
|
97
|
+
Jektex is highly configurable via your `_config.yml` file.
|
|
98
|
+
Unknown options and invalid values are reported during the build and fall back to their defaults.
|
|
77
99
|
|
|
78
100
|
**Disabling cache**
|
|
79
|
-
You can disable caching with `disable_disk_cache
|
|
101
|
+
You can disable caching with the `disable_disk_cache` option.
|
|
80
102
|
Caching is enabled by default.
|
|
103
|
+
This is Jekyll's own option, so unlike the options below it belongs
|
|
104
|
+
at the top level of `_config.yml`, not under the `jektex` key:
|
|
105
|
+
```yaml
|
|
106
|
+
disable_disk_cache: true
|
|
107
|
+
```
|
|
81
108
|
You can find more information on [Jekyll's official website](https://jekyllrb.com/docs/configuration/options/).
|
|
82
109
|
|
|
83
110
|
**Setting cache location**
|
|
@@ -124,6 +151,15 @@ jektex:
|
|
|
124
151
|
And yes, you have to escape the backlash (`\`) with another backlash.
|
|
125
152
|
This is due to the [yaml specification](https://yaml.org/).
|
|
126
153
|
|
|
154
|
+
You can define macros with parameters:
|
|
155
|
+
```yaml
|
|
156
|
+
jektex:
|
|
157
|
+
macros:
|
|
158
|
+
- ["\\vec", "\\mathbf{#1}"]
|
|
159
|
+
- ["\\addBar", "\\bar{#1}"]
|
|
160
|
+
```
|
|
161
|
+
This simulates behaviour of LaTeX `\newcommand`.
|
|
162
|
+
|
|
127
163
|
**Silencing Jektex output**
|
|
128
164
|
Jektex periodically informs the user about rendered/cached equations.
|
|
129
165
|
If this is not desired, you can set the `silent` option (`false` by default).
|
|
@@ -132,6 +168,33 @@ jektex:
|
|
|
132
168
|
silent: true
|
|
133
169
|
```
|
|
134
170
|
|
|
171
|
+
**KaTeX options**
|
|
172
|
+
You can pass any [KaTeX rendering option](https://katex.org/docs/options) to the renderer
|
|
173
|
+
through the `katex_options` key. Write the keys exactly as the KaTeX documentation spells them:
|
|
174
|
+
```yaml
|
|
175
|
+
jektex:
|
|
176
|
+
katex_options:
|
|
177
|
+
trust: false
|
|
178
|
+
output: htmlAndMathml
|
|
179
|
+
strict: warn
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
The most useful options are:
|
|
183
|
+
|
|
184
|
+
- `trust` toggles features KaTeX deems potentially unsafe (`false` by default), namely
|
|
185
|
+
`\url`, `\href`, `\includegraphics`, `\htmlClass`, `\htmlId`, `\htmlStyle` and `\htmlData`.
|
|
186
|
+
- `output: html` halves the size of every rendered formula by omitting the invisible MathML
|
|
187
|
+
copy. Be aware that the MathML is what screen readers use.
|
|
188
|
+
- `maxExpand` limits macro expansion and protects your build from runaway recursive macros.
|
|
189
|
+
|
|
190
|
+
Jektex controls `displayMode`, `macros`, `throwOnError` and `globalGroup` itself,
|
|
191
|
+
so these keys are ignored (define your macros with the `macros` option above).
|
|
192
|
+
Values of the documented KaTeX options are checked: an invalid value is reported
|
|
193
|
+
and KaTeX's own default is used instead. Option names jektex does not know
|
|
194
|
+
(for example ones added by newer KaTeX versions) are passed through unchecked.
|
|
195
|
+
Changing any KaTeX option invalidates the cache and the next build re-renders everything once.
|
|
196
|
+
|
|
197
|
+
|
|
135
198
|
**Complete examples**
|
|
136
199
|
Recommended config:
|
|
137
200
|
```yaml
|
|
@@ -139,6 +202,8 @@ jektex:
|
|
|
139
202
|
cache_dir: ".jektex-cache"
|
|
140
203
|
ignore: ["*.xml"]
|
|
141
204
|
silent: false
|
|
205
|
+
katex_options:
|
|
206
|
+
trust: false
|
|
142
207
|
macros:
|
|
143
208
|
- ["\\Q", "\\mathbb{Q}"]
|
|
144
209
|
- ["\\C", "\\mathbb{C}"]
|
|
@@ -150,6 +215,7 @@ jektex:
|
|
|
150
215
|
cache_dir: ".jekyll-cache"
|
|
151
216
|
ignore: []
|
|
152
217
|
silent: false
|
|
218
|
+
katex_options: {}
|
|
153
219
|
macros: []
|
|
154
220
|
```
|
|
155
221
|
|
|
@@ -178,9 +244,9 @@ plugins:
|
|
|
178
244
|
|
|
179
245
|
and don't forget to add `katex.min.css` to you HTML head:
|
|
180
246
|
```html
|
|
181
|
-
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.
|
|
247
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.17.0/dist/katex.min.css" integrity="sha384-vlBdW0r3AcZO/HboRPznQNowvexd3fY8qHOWkBi5q7KGgqJ+F48+DceybYmrVbmB" crossorigin="anonymous">
|
|
182
248
|
```
|
|
183
|
-
It is much better practice to download the [**css** file](https://cdn.jsdelivr.net/npm/katex@0.
|
|
249
|
+
It is much better practice to download the [**css** file](https://cdn.jsdelivr.net/npm/katex@0.17.0/dist/katex.min.css) and load it as an asset from your server directly.
|
|
184
250
|
You can find more information on [KaTeX's website](https://katex.org/docs/browser.html).
|
|
185
251
|
|
|
186
252
|
## Contributions and bug reports
|
data/lib/jektex/cache.rb
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
require 'digest'
|
|
2
|
+
require 'fileutils'
|
|
3
|
+
|
|
4
|
+
module Jektex
|
|
5
|
+
# Disk cache of rendered expressions. The cache file additionally stores
|
|
6
|
+
# the macro table it was rendered with, so expressions using an edited
|
|
7
|
+
# macro can be invalidated individually, and a fingerprint of everything
|
|
8
|
+
# else that affects KaTeX output, so a KaTeX or configuration change
|
|
9
|
+
# discards the whole cache instead of serving stale HTML.
|
|
10
|
+
class Cache
|
|
11
|
+
FORMAT = 2
|
|
12
|
+
|
|
13
|
+
attr_reader :updated_global_macros
|
|
14
|
+
|
|
15
|
+
# names of macros whose definition differs between the two tables
|
|
16
|
+
def self.updated_macros_between(current_macros, cached_macros)
|
|
17
|
+
current = current_macros || Hash.new
|
|
18
|
+
cached = cached_macros || Hash.new
|
|
19
|
+
(current.keys | cached.keys).reject { |name| current[name] == cached[name] }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def initialize(config, reporter: nil)
|
|
23
|
+
@config = config
|
|
24
|
+
@reporter = reporter
|
|
25
|
+
@entries = Hash.new
|
|
26
|
+
@updated_global_macros = Array.new
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def load
|
|
30
|
+
payload = read_payload
|
|
31
|
+
if payload
|
|
32
|
+
@entries = payload["entries"]
|
|
33
|
+
@updated_global_macros =
|
|
34
|
+
self.class.updated_macros_between(@config.global_macros, payload["macros"])
|
|
35
|
+
end
|
|
36
|
+
return self
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def fetch(expression, display_mode)
|
|
40
|
+
return nil if @updated_global_macros.any? { |macro| expression.include?(macro) }
|
|
41
|
+
return @entries[key_for(expression, display_mode)]
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def store(expression, display_mode, html)
|
|
45
|
+
@entries[key_for(expression, display_mode)] = html
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def size
|
|
49
|
+
return @entries.size
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def save
|
|
53
|
+
return if @config.disable_disk_cache
|
|
54
|
+
path = @config.path_to_cache_file
|
|
55
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
56
|
+
payload = { "format" => FORMAT,
|
|
57
|
+
"fingerprint" => fingerprint,
|
|
58
|
+
"macros" => @config.global_macros,
|
|
59
|
+
"entries" => @entries }
|
|
60
|
+
# write to a temporary file first so an interrupted write
|
|
61
|
+
# can never leave a truncated cache behind
|
|
62
|
+
temporary_path = path + ".tmp"
|
|
63
|
+
File.open(temporary_path, "wb") { |file| Marshal.dump(payload, file) }
|
|
64
|
+
File.rename(temporary_path, path)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
def read_payload
|
|
70
|
+
path = @config.path_to_cache_file
|
|
71
|
+
return nil unless File.exist?(path)
|
|
72
|
+
payload = File.open(path, "rb") { |file| Marshal.load(file) }
|
|
73
|
+
unless valid?(payload)
|
|
74
|
+
@reporter&.info("cache file is invalid and will be rebuilt")
|
|
75
|
+
return nil
|
|
76
|
+
end
|
|
77
|
+
unless payload["fingerprint"] == fingerprint
|
|
78
|
+
@reporter&.info("cache reset (configuration or KaTeX changed)")
|
|
79
|
+
return nil
|
|
80
|
+
end
|
|
81
|
+
return payload
|
|
82
|
+
rescue StandardError
|
|
83
|
+
@reporter&.info("cache file is invalid and will be rebuilt")
|
|
84
|
+
return nil
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def valid?(payload)
|
|
88
|
+
payload.is_a?(Hash) &&
|
|
89
|
+
payload["format"] == FORMAT &&
|
|
90
|
+
payload["entries"].is_a?(Hash) &&
|
|
91
|
+
payload["macros"].is_a?(Hash)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def key_for(expression, display_mode)
|
|
95
|
+
Digest::SHA2.hexdigest(expression) + (display_mode ? ":display" : ":inline")
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# everything that changes KaTeX output (except macros, which are diffed
|
|
99
|
+
# individually) must be part of this fingerprint; katex_options covers
|
|
100
|
+
# all pass-through options, jektex-side additions must be appended
|
|
101
|
+
def fingerprint
|
|
102
|
+
@fingerprint ||= [FORMAT,
|
|
103
|
+
Digest::SHA2.file(@config.path_to_katex_js).hexdigest[0, 16],
|
|
104
|
+
@config.katex_options.sort.inspect].join("|")
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
module Jektex
|
|
2
|
+
class Config
|
|
3
|
+
# KaTeX options jektex needs to control itself: displayMode is decided
|
|
4
|
+
# per expression by its delimiters, macros have their own config key,
|
|
5
|
+
# throwOnError drives the error reporting pipeline, and globalGroup
|
|
6
|
+
# would make rendering order-dependent, which breaks caching
|
|
7
|
+
RESERVED_KATEX_OPTIONS = ["displayMode", "macros", "throwOnError", "globalGroup"].freeze
|
|
8
|
+
|
|
9
|
+
KNOWN_OPTIONS = ["cache_dir", "ignore", "silent", "macros", "katex_options"].freeze
|
|
10
|
+
|
|
11
|
+
# value checks for the KaTeX options documented at katex.org/docs/options;
|
|
12
|
+
# keys not listed here are passed to KaTeX unchecked, so options added
|
|
13
|
+
# by future KaTeX versions keep working without a jektex update
|
|
14
|
+
KATEX_OPTION_VALIDATIONS = {
|
|
15
|
+
"output" => ['"html", "mathml" or "htmlAndMathml"', '"htmlAndMathml"',
|
|
16
|
+
->(value) { ["html", "mathml", "htmlAndMathml"].include?(value) }],
|
|
17
|
+
"leqno" => ["true or false", "false", ->(value) { [true, false].include?(value) }],
|
|
18
|
+
"fleqn" => ["true or false", "false", ->(value) { [true, false].include?(value) }],
|
|
19
|
+
"errorColor" => ["a color string", '"#cc0000"', ->(value) { value.is_a?(String) }],
|
|
20
|
+
"minRuleThickness" => ["a number", "the KaTeX default", ->(value) { value.is_a?(Numeric) }],
|
|
21
|
+
"colorIsTextColor" => ["true or false", "false", ->(value) { [true, false].include?(value) }],
|
|
22
|
+
"maxSize" => ["a number", "unlimited", ->(value) { value.is_a?(Numeric) }],
|
|
23
|
+
"maxExpand" => ["a number", "1000", ->(value) { value.is_a?(Numeric) }],
|
|
24
|
+
"strict" => ['true, false, "ignore", "warn" or "error"', '"warn"',
|
|
25
|
+
->(value) { [true, false, "ignore", "warn", "error"].include?(value) }],
|
|
26
|
+
"trust" => ["true or false", "false", ->(value) { [true, false].include?(value) }]
|
|
27
|
+
}.freeze
|
|
28
|
+
|
|
29
|
+
attr_reader :path_to_katex_js
|
|
30
|
+
attr_reader :disable_disk_cache
|
|
31
|
+
attr_reader :ignore
|
|
32
|
+
attr_reader :console_indent
|
|
33
|
+
attr_reader :silent
|
|
34
|
+
attr_reader :front_matter_tag
|
|
35
|
+
attr_reader :katex_options
|
|
36
|
+
attr_reader :global_macros
|
|
37
|
+
attr_reader :markdown_extensions
|
|
38
|
+
# problems found while reading the configuration; the plugin prints
|
|
39
|
+
# them at startup, so this class can stay free of console output
|
|
40
|
+
attr_reader :warnings
|
|
41
|
+
|
|
42
|
+
def initialize(config)
|
|
43
|
+
@path_to_katex_js = File.join(__dir__, "/katex.min.js")
|
|
44
|
+
|
|
45
|
+
@cache_dir = ".jekyll-cache"
|
|
46
|
+
@cache_file = "jektex-cache.marshal"
|
|
47
|
+
@disable_disk_cache = false
|
|
48
|
+
|
|
49
|
+
@ignore = Array.new
|
|
50
|
+
@console_indent = " " * 13
|
|
51
|
+
@silent = false
|
|
52
|
+
|
|
53
|
+
@front_matter_tag = "jektex"
|
|
54
|
+
@katex_options = Hash.new
|
|
55
|
+
|
|
56
|
+
@markdown_ext = "markdown,mkdown,mkdn,mkd,md"
|
|
57
|
+
|
|
58
|
+
@global_macros = Hash.new
|
|
59
|
+
@warnings = Array.new
|
|
60
|
+
|
|
61
|
+
if config.is_a?(Hash)
|
|
62
|
+
update_from_jekyll_config(config)
|
|
63
|
+
end
|
|
64
|
+
add_jektex_logo_macro
|
|
65
|
+
@ignore.append("#{@cache_dir}/*")
|
|
66
|
+
@markdown_extensions = @markdown_ext.to_s.split(",").map { |ext| ".#{ext.strip.downcase}" }
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def path_to_cache_file
|
|
70
|
+
return File.join(@cache_dir, @cache_file)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def number_of_global_macros
|
|
74
|
+
return @global_macros.length - 1
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
def update_from_jekyll_config(config)
|
|
79
|
+
if config.key?("jektex")
|
|
80
|
+
jektex_config = config["jektex"]
|
|
81
|
+
if jektex_config.is_a?(Hash)
|
|
82
|
+
warn_about_unknown_options(jektex_config)
|
|
83
|
+
@cache_dir = checked_option(jektex_config, "cache_dir", @cache_dir, "a string") do |value|
|
|
84
|
+
value.is_a?(String)
|
|
85
|
+
end
|
|
86
|
+
@ignore = checked_option(jektex_config, "ignore", @ignore, "a list of file patterns") do |value|
|
|
87
|
+
value.is_a?(Array) && value.all? { |pattern| pattern.is_a?(String) }
|
|
88
|
+
end
|
|
89
|
+
@silent = checked_option(jektex_config, "silent", @silent, "true or false") do |value|
|
|
90
|
+
value == true || value == false
|
|
91
|
+
end
|
|
92
|
+
read_katex_options(jektex_config)
|
|
93
|
+
read_macros(jektex_config)
|
|
94
|
+
else
|
|
95
|
+
@warnings.append('the "jektex" configuration must be a mapping of options, ignoring it')
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
@disable_disk_cache = config["disable_disk_cache"] if config.has_key?("disable_disk_cache")
|
|
100
|
+
@markdown_ext = config["markdown_ext"] if config.has_key?("markdown_ext")
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def warn_about_unknown_options(jektex_config)
|
|
104
|
+
jektex_config.each_key do |key|
|
|
105
|
+
next if KNOWN_OPTIONS.include?(key)
|
|
106
|
+
@warnings.append("unknown option \"#{key}\" " \
|
|
107
|
+
"(known options: #{KNOWN_OPTIONS.join(", ")})")
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# returns the configured value when the validation block accepts it,
|
|
112
|
+
# otherwise warns, names the default and returns it
|
|
113
|
+
def checked_option(jektex_config, name, default, expectation)
|
|
114
|
+
return default unless jektex_config.key?(name)
|
|
115
|
+
value = jektex_config[name]
|
|
116
|
+
return value if yield(value)
|
|
117
|
+
@warnings.append("option \"#{name}\" must be #{expectation}, " \
|
|
118
|
+
"falling back to default: #{default.inspect}")
|
|
119
|
+
return default
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def read_katex_options(jektex_config)
|
|
123
|
+
return unless jektex_config.key?("katex_options")
|
|
124
|
+
options = jektex_config["katex_options"]
|
|
125
|
+
unless options.is_a?(Hash)
|
|
126
|
+
@warnings.append('option "katex_options" must be a mapping of KaTeX options, ' \
|
|
127
|
+
"falling back to default: {}")
|
|
128
|
+
return
|
|
129
|
+
end
|
|
130
|
+
options.each_key do |key|
|
|
131
|
+
if RESERVED_KATEX_OPTIONS.include?(key.to_s)
|
|
132
|
+
@warnings.append("KaTeX option \"#{key}\" is controlled by jektex and was ignored")
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
@katex_options = options.reject { |key, _value| RESERVED_KATEX_OPTIONS.include?(key.to_s) }
|
|
136
|
+
KATEX_OPTION_VALIDATIONS.each do |name, (expectation, default_description, valid)|
|
|
137
|
+
next unless @katex_options.key?(name)
|
|
138
|
+
next if valid.call(@katex_options[name])
|
|
139
|
+
@warnings.append("KaTeX option \"#{name}\" must be #{expectation}, " \
|
|
140
|
+
"falling back to KaTeX default: #{default_description}")
|
|
141
|
+
@katex_options.delete(name)
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def read_macros(jektex_config)
|
|
146
|
+
return unless jektex_config.has_key?("macros")
|
|
147
|
+
macro_list = jektex_config["macros"]
|
|
148
|
+
valid = macro_list.is_a?(Array) && macro_list.all? do |pair|
|
|
149
|
+
pair.is_a?(Array) && pair.size == 2 && pair.all? { |part| part.is_a?(String) }
|
|
150
|
+
end
|
|
151
|
+
unless valid
|
|
152
|
+
@warnings.append('option "macros" must be a list of [name, definition] pairs of strings, ' \
|
|
153
|
+
"falling back to default: no macros")
|
|
154
|
+
return
|
|
155
|
+
end
|
|
156
|
+
for macro_definition in macro_list
|
|
157
|
+
@global_macros[macro_definition[0]] = macro_definition[1]
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def add_jektex_logo_macro
|
|
162
|
+
@global_macros['\jektex'] =
|
|
163
|
+
'\text{\raisebox{-0.55ex}{J}\kern{-0.3ex}E\kern{-0.25ex}\raisebox{-0.5ex}{K}\kern{-0.7ex}}\TeX'
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
end
|
|
167
|
+
end
|
data/lib/jektex/hooks.rb
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Jekyll integration. Everything else in this gem is plain Ruby;
|
|
2
|
+
# this file is the only one that touches Jekyll::Hooks.
|
|
3
|
+
|
|
4
|
+
module Jektex
|
|
5
|
+
class << self
|
|
6
|
+
attr_accessor :config, :cache, :renderer, :page_processor, :reporter
|
|
7
|
+
end
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
# Fires once per process (for jekyll build and jekyll serve alike),
|
|
11
|
+
# at the end of Site#initialize.
|
|
12
|
+
Jekyll::Hooks.register :site, :after_init do |site|
|
|
13
|
+
Jektex.config = Jektex::Config.new(site.config || Hash.new)
|
|
14
|
+
Jektex.reporter = Jektex::Reporter.new(Jektex.config)
|
|
15
|
+
Jektex.config.warnings.each { |warning| Jektex.reporter.warn(warning) }
|
|
16
|
+
Jektex.cache = Jektex::Cache.new(Jektex.config, reporter: Jektex.reporter).load
|
|
17
|
+
Jektex.renderer = Jektex::Renderer.new(Jektex.config)
|
|
18
|
+
Jektex.page_processor = Jektex::PageProcessor.new(config: Jektex.config,
|
|
19
|
+
cache: Jektex.cache,
|
|
20
|
+
renderer: Jektex.renderer,
|
|
21
|
+
reporter: Jektex.reporter)
|
|
22
|
+
Jektex.reporter.macro_summary(Jektex.config.number_of_global_macros,
|
|
23
|
+
Jektex.cache.updated_global_macros.size)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# LaTeX notation (\( \) and \[ \]) in raw content, before Liquid/kramdown:
|
|
27
|
+
# expressions are protected behind inert tokens so markdown cannot touch them.
|
|
28
|
+
Jekyll::Hooks.register [:pages, :documents], :pre_render do |page|
|
|
29
|
+
page.content = Jektex.page_processor.process_content(page)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# After conversion the HTML structure is known: tokens inside code markup
|
|
33
|
+
# are restored to their source text, everything else is rendered — including
|
|
34
|
+
# the \(..\)/\[..\] delimiters kramdown produces from its $$..$$ notation
|
|
35
|
+
# and the $$..$$ kramdown leaves untouched inside raw HTML blocks.
|
|
36
|
+
Jekyll::Hooks.register [:pages, :documents], :post_render do |page|
|
|
37
|
+
page.output = Jektex.page_processor.process_output(page)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Fires once per rebuild in watch mode — and also once during
|
|
41
|
+
# Site#initialize BEFORE after_init has run, hence the safe navigation.
|
|
42
|
+
Jekyll::Hooks.register :site, :after_reset do |_site|
|
|
43
|
+
Jektex.page_processor&.reset_counters
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
Jekyll::Hooks.register :site, :post_write do |_site|
|
|
47
|
+
# print stats once more so the error log cannot overwrite them
|
|
48
|
+
Jektex.reporter.finish(Jektex.page_processor.rendered_count,
|
|
49
|
+
Jektex.page_processor.cache_hit_count)
|
|
50
|
+
Jektex.cache.save
|
|
51
|
+
end
|