jekyll-mathjax-csp 1.0.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 +7 -0
- data/README.md +70 -0
- data/lib/jekyll-mathjax-csp.rb +217 -0
- metadata +74 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 7e8d81d890fe7c24488fd6fd9573d86504a184d0
|
4
|
+
data.tar.gz: 78b3d2fdf8c0065dfbbb6a47d1f3d84652e2e94f
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: b616058752d9bd91f13d962e0f4a0e4a6d7a8436103d44e9168cea473fffcf20d636a5367fc46313e7103bc34096ed5acb8833d96bde791ab5e8579d651b1e26
|
7
|
+
data.tar.gz: c92c0463764c80f2ddeac44b4b1f4e9a7c93581c13b3aa9968b1f9cc63fcce15fc9521a6563065c818f111f40ac178f8b9dd05b6f4a80814818ede6c29a15a8c
|
data/README.md
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
# jekyll-mathjax-csp
|
2
|
+
|
3
|
+
Render math on the server using [MathJax-node](https://github.com/mathjax/MathJax-node), while maintaining a strict [Content Security Policy (CSP)](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) without `'unsafe-inline'`.
|
4
|
+
|
5
|
+
While MathJax is well equipped to render beautiful math in a browser, letting it run in the client has two distinctive disadvantages: It is quite CPU-intensive and crucially relies on inline `style` attributes and elements. This Jekyll plugin aims to resolve both issues at once by rendering formulas to SVG images on the server, extracting all generated `style` attributes into a single `<style>` element in the head of the page and computing a hash over its content that can then be added as a CSP `style-src`.
|
6
|
+
|
7
|
+
The plugin runs the output of Jekyll's markdown parser [kramdown](http://kramdown.gettalong.org/) through the CLI converter `mjpage` offered by the npm package [`mathjax-node-page`](https://github.com/pkra/mathjax-node-page) and thus behaves exactly as client-side MathJax in SVG rendering mode would.
|
8
|
+
|
9
|
+
## Usage
|
10
|
+
|
11
|
+
1. Install the npm package `mathjax-node-page` from your top-level Jekyll directory:
|
12
|
+
|
13
|
+
```bash
|
14
|
+
npm init -f # only if you don't have a package.json yet
|
15
|
+
npm install mathjax-node-page@2.X
|
16
|
+
```
|
17
|
+
|
18
|
+
2. Install `jekyll-mathjax-csp`:
|
19
|
+
|
20
|
+
```bash
|
21
|
+
gem install jekyll-mathjax-csp
|
22
|
+
```
|
23
|
+
|
24
|
+
3. Ensure that your `_config.yml` contains the following settings:
|
25
|
+
|
26
|
+
```yaml
|
27
|
+
plugins:
|
28
|
+
- jekyll-mathjax-csp
|
29
|
+
|
30
|
+
exclude:
|
31
|
+
- node_modules
|
32
|
+
- package.json
|
33
|
+
- package-lock.json
|
34
|
+
```
|
35
|
+
|
36
|
+
4. Add the `{% mathjax_sources %}` Liquid tag where you want the CSP `'sha256-...'` hashes for `<style>` elements to be emitted. Don't forget to add the YAML front matter (two lines of `---`) to such files. If you specify your CSP in a different way, add the `style-src` sources the plugins prints to the console during build.
|
37
|
+
|
38
|
+
5. Include beautiful math in your posts!
|
39
|
+
|
40
|
+
## Dependencies
|
41
|
+
|
42
|
+
* `mathjax-node-page` (npm): 2.0+
|
43
|
+
* `html-pipeline`: 2.3+
|
44
|
+
* `jekyll`: 3.0+
|
45
|
+
|
46
|
+
## Configuration
|
47
|
+
|
48
|
+
'mathjax-node-page' adds a fixed inline stylesheet to every page containing math. If you want to serve this stylesheet as an external `.css`, you can advise the plugin to strip it from the output by adding the following lines to your `_config.yml`:
|
49
|
+
|
50
|
+
```yaml
|
51
|
+
mathjax_csp:
|
52
|
+
strip_css: true
|
53
|
+
```
|
54
|
+
|
55
|
+
## Local testing
|
56
|
+
|
57
|
+
If you want to try out your CSP locally, you can specify headers in your `_config.yml`:
|
58
|
+
|
59
|
+
```yaml
|
60
|
+
webrick:
|
61
|
+
headers:
|
62
|
+
Content-Security-Policy: >-
|
63
|
+
default-src 'none'; script-src ...
|
64
|
+
```
|
65
|
+
|
66
|
+
It is unfortunately not possible to have Liquid tags in `_config.yml`, so you will have to update your CSP manually. Don't forget to restart `jekyll` for it to pick up the config changes.
|
67
|
+
|
68
|
+
## License
|
69
|
+
|
70
|
+
MIT
|
@@ -0,0 +1,217 @@
|
|
1
|
+
# Server-side, CSP-aware math rendering for Jekyll using mathjax-node-page
|
2
|
+
#
|
3
|
+
# The MIT License (MIT)
|
4
|
+
# =====================
|
5
|
+
#
|
6
|
+
# Copyright © 2018 Fabian Henneke
|
7
|
+
#
|
8
|
+
# Permission is hereby granted, free of charge, to any person
|
9
|
+
# obtaining a copy of this software and associated documentation
|
10
|
+
# files (the “Software”), to deal in the Software without
|
11
|
+
# restriction, including without limitation the rights to use,
|
12
|
+
# copy, modify, merge, publish, distribute, sublicense, and/or sell
|
13
|
+
# copies of the Software, and to permit persons to whom the
|
14
|
+
# Software is furnished to do so, subject to the following
|
15
|
+
# conditions:
|
16
|
+
#
|
17
|
+
# The above copyright notice and this permission notice shall be
|
18
|
+
# included in all copies or substantial portions of the Software.
|
19
|
+
#
|
20
|
+
# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
|
21
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
22
|
+
# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
23
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
24
|
+
# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
25
|
+
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
26
|
+
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
27
|
+
# OTHER DEALINGS IN THE SOFTWARE.
|
28
|
+
|
29
|
+
require "html/pipeline"
|
30
|
+
require "jekyll"
|
31
|
+
|
32
|
+
require "digest"
|
33
|
+
require "open3"
|
34
|
+
require "securerandom"
|
35
|
+
require "set"
|
36
|
+
|
37
|
+
module Jekyll
|
38
|
+
|
39
|
+
# Run Jekyll documents through mathjax-node-page, transform style attributes into inline style
|
40
|
+
# tags and compute their hashes
|
41
|
+
class Mathifier
|
42
|
+
MATH_TAG_REGEX = /<script[^>]*type="math\/tex/i
|
43
|
+
|
44
|
+
class << self
|
45
|
+
attr_accessor :csp_hashes
|
46
|
+
|
47
|
+
# Extract all style attributes from SVG elements and replace them by a new CSS class with a
|
48
|
+
# deterministic name
|
49
|
+
def extractStyleAttributes(parsed_doc)
|
50
|
+
style_attributes = {}
|
51
|
+
svg_tags = parsed_doc.css("svg[style]")
|
52
|
+
for svg_tag in svg_tags do
|
53
|
+
style_attribute = svg_tag["style"]
|
54
|
+
digest = Digest::MD5.hexdigest(style_attribute)[0..15]
|
55
|
+
style_attributes[digest] = style_attribute
|
56
|
+
|
57
|
+
digest_class = "mathjax-inline-#{digest}"
|
58
|
+
svg_tag["class"] = "#{svg_tag["class"] || ""} #{digest_class}"
|
59
|
+
svg_tag.remove_attribute("style")
|
60
|
+
end
|
61
|
+
return style_attributes
|
62
|
+
end
|
63
|
+
|
64
|
+
# Compute a CSP hash source (using SHA256)
|
65
|
+
def hashStyleTag(style_tag)
|
66
|
+
csp_digest = "'sha256-#{Digest::SHA256.base64digest(style_tag.content)}'"
|
67
|
+
style_tag.add_previous_sibling("<!-- #{csp_digest} -->")
|
68
|
+
@csp_hashes.add(csp_digest)
|
69
|
+
end
|
70
|
+
|
71
|
+
# Compile all style attributes into CSS classes in a single <style> element in the head
|
72
|
+
def compileStyleElement(parsed_doc, style_attributes)
|
73
|
+
style_content = ""
|
74
|
+
style_attributes.each do |digest, style_attribute|
|
75
|
+
style_content += ".mathjax-inline-#{digest}{#{style_attribute}}"
|
76
|
+
end
|
77
|
+
style_tag = parsed_doc.at_css("head").add_child("<style>#{style_content}</style>")[0]
|
78
|
+
hashStyleTag(style_tag)
|
79
|
+
end
|
80
|
+
|
81
|
+
# Run mathjax-node-page on a String containing an HTML doc
|
82
|
+
def run_mjpage(output)
|
83
|
+
mathified = ""
|
84
|
+
exit_status = 0
|
85
|
+
begin
|
86
|
+
Open3.popen2("node_modules/mathjax-node-page/bin/mjpage") {|i,o,t|
|
87
|
+
i.print output
|
88
|
+
i.close
|
89
|
+
o.each {|line|
|
90
|
+
mathified.concat(line)
|
91
|
+
}
|
92
|
+
exit_status = t.value
|
93
|
+
}
|
94
|
+
return mathified unless exit_status != 0
|
95
|
+
Jekyll.logger.abort_with "mathjax_csp:", "'node_modules/mathjax-node-page/mjpage' not found"
|
96
|
+
rescue
|
97
|
+
Jekyll.logger.abort_with "mathjax_csp:", "Failed to execute 'node_modules/mathjax-node-page/mjpage'"
|
98
|
+
end
|
99
|
+
|
100
|
+
end
|
101
|
+
|
102
|
+
# Render math
|
103
|
+
def mathify(doc, config)
|
104
|
+
return unless MATH_TAG_REGEX.match?(doc.output)
|
105
|
+
|
106
|
+
Jekyll.logger.info "Rendering math:", doc.relative_path
|
107
|
+
parsed_doc = Nokogiri::HTML::Document.parse(doc.output)
|
108
|
+
# Ensure that all styles we pick up weren't present before mjpage ran
|
109
|
+
unless parsed_doc.css("svg[style]").empty?()
|
110
|
+
Jekyll.logger.error "mathjax_csp:", "Inline style on <svg> element present before running mjpage"
|
111
|
+
Jekyll.logger.abort_with "", "due to a misconfiguration or server-side style injection."
|
112
|
+
end
|
113
|
+
|
114
|
+
mjpage_output = run_mjpage(doc.output)
|
115
|
+
parsed_doc = Nokogiri::HTML::Document.parse(mjpage_output)
|
116
|
+
last_child = parsed_doc.at_css("head").last_element_child()
|
117
|
+
if last_child.name == "style"
|
118
|
+
# Set strip_css to true in _config.yml if you load the styles mjpage adds to the head
|
119
|
+
# from an external *.css file
|
120
|
+
if config["strip_css"]
|
121
|
+
Jekyll.logger.info "", "Remember to <link> in external stylesheet!"
|
122
|
+
last_child.remove
|
123
|
+
else
|
124
|
+
hashStyleTag(last_child)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
style_attributes = extractStyleAttributes(parsed_doc)
|
129
|
+
compileStyleElement(parsed_doc, style_attributes)
|
130
|
+
doc.output = parsed_doc.to_html
|
131
|
+
end
|
132
|
+
|
133
|
+
def mathable?(doc)
|
134
|
+
(doc.is_a?(Jekyll::Page) || doc.write?) &&
|
135
|
+
doc.output_ext == ".html" || (doc.permalink && doc.permalink.end_with?("/"))
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
# Register the page with the {% mathjax_csp_sources %} Liquid tag for the second pass and
|
141
|
+
# temporarily emit a placeholder, later to be replaced by the list of MathJax-related CSP hashes
|
142
|
+
class MathJaxSourcesTag < Liquid::Tag
|
143
|
+
|
144
|
+
class << self
|
145
|
+
attr_accessor :final_source_list, :second_pass, :second_pass_docs, :unrendered_docs
|
146
|
+
end
|
147
|
+
|
148
|
+
def initialize(tag_name, text, tokens)
|
149
|
+
super
|
150
|
+
end
|
151
|
+
|
152
|
+
def render(context)
|
153
|
+
page = context.registers[:page]
|
154
|
+
if self.class.second_pass
|
155
|
+
return self.class.final_source_list
|
156
|
+
else
|
157
|
+
self.class.second_pass_docs.add(page["path"])
|
158
|
+
return ""
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
Liquid::Template.register_tag("mathjax_csp_sources", Jekyll::MathJaxSourcesTag)
|
165
|
+
|
166
|
+
# Set up plugin config
|
167
|
+
Jekyll::Hooks.register :site, :pre_render do |site|
|
168
|
+
# A set of CSP hash sources to be added as style sources; populated automatically
|
169
|
+
Jekyll::Mathifier.csp_hashes = Set.new([])
|
170
|
+
# This is the first pass (mathify & collect hashes), the second pass (insert hashes into CSP
|
171
|
+
# rules) is triggered manually
|
172
|
+
Jekyll::MathJaxSourcesTag.second_pass = false
|
173
|
+
# A set of Jekyll documents to which the hash sources should be added; populated automatically
|
174
|
+
Jekyll::MathJaxSourcesTag.second_pass_docs = Set.new([])
|
175
|
+
# The original file content of documents
|
176
|
+
Jekyll::MathJaxSourcesTag.unrendered_docs = {}
|
177
|
+
end
|
178
|
+
|
179
|
+
# Keep original (Markdown) content of documents around for the second rendering pass
|
180
|
+
Jekyll::Hooks.register [:documents, :pages], :pre_render do |doc|
|
181
|
+
Jekyll::MathJaxSourcesTag.unrendered_docs[doc.relative_path] = doc.content
|
182
|
+
end
|
183
|
+
|
184
|
+
# Replace math blocks with SVG renderings using mathjax-node-page and collect inline styles in a
|
185
|
+
# single <style> element
|
186
|
+
Jekyll::Hooks.register [:documents, :pages], :post_render do |doc|
|
187
|
+
if Jekyll::Mathifier.mathable?(doc)
|
188
|
+
Jekyll::Mathifier.mathify(doc, doc.site.config["mathjax_csp"])
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
# Run over all documents with {% mathjax_sources %} Liquid tags again and insert the list of CSP
|
193
|
+
# hash sources coming from MathJax styles
|
194
|
+
Jekyll::Hooks.register :site, :post_render do |site, payload|
|
195
|
+
Jekyll::MathJaxSourcesTag.second_pass = true
|
196
|
+
Jekyll::MathJaxSourcesTag.final_source_list = Jekyll::Mathifier.csp_hashes.to_a().join(" ")
|
197
|
+
if Jekyll::MathJaxSourcesTag.second_pass_docs.empty?()
|
198
|
+
Jekyll.logger.info "mathjax_csp:", "Add the following to the style-src part of your CSP:"
|
199
|
+
Jekyll.logger.info "", Jekyll::MathJaxSourcesTag.final_source_list
|
200
|
+
else
|
201
|
+
second_pass_docs_str = Jekyll::MathJaxSourcesTag.second_pass_docs.to_a().join(" ")
|
202
|
+
Jekyll.logger.info "Adding CSP sources:", second_pass_docs_str
|
203
|
+
rerender = proc { |docs, uses_absolute_path|
|
204
|
+
docs.each do |doc|
|
205
|
+
relative_path = uses_absolute_path ? doc.relative_path : doc.path
|
206
|
+
if Jekyll::MathJaxSourcesTag.second_pass_docs.include?(relative_path)
|
207
|
+
# Rerender the page
|
208
|
+
doc.content = Jekyll::MathJaxSourcesTag.unrendered_docs[relative_path]
|
209
|
+
doc.output = Jekyll::Renderer.new(site, doc, payload).run()
|
210
|
+
doc.trigger_hooks(:post_render)
|
211
|
+
end
|
212
|
+
end
|
213
|
+
}
|
214
|
+
rerender.call(site.pages, false)
|
215
|
+
rerender.call(site.docs_to_write, true)
|
216
|
+
end
|
217
|
+
end
|
metadata
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: jekyll-mathjax-csp
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Fabian Henneke
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2018-02-04 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: html-pipeline
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '2.3'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '2.3'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: jekyll
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '3.0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '3.0'
|
41
|
+
description: Server-side MathJax rendering for Jekyll with a strict CSP
|
42
|
+
email:
|
43
|
+
executables: []
|
44
|
+
extensions: []
|
45
|
+
extra_rdoc_files:
|
46
|
+
- README.md
|
47
|
+
files:
|
48
|
+
- README.md
|
49
|
+
- lib/jekyll-mathjax-csp.rb
|
50
|
+
homepage: https://github.com/FabianHenneke/jekyll-mathjax-csp
|
51
|
+
licenses:
|
52
|
+
- MIT
|
53
|
+
metadata: {}
|
54
|
+
post_install_message:
|
55
|
+
rdoc_options: []
|
56
|
+
require_paths:
|
57
|
+
- lib
|
58
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
59
|
+
requirements:
|
60
|
+
- - ">="
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: '0'
|
63
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - ">="
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '0'
|
68
|
+
requirements: []
|
69
|
+
rubyforge_project:
|
70
|
+
rubygems_version: 2.6.13
|
71
|
+
signing_key:
|
72
|
+
specification_version: 4
|
73
|
+
summary: Server-side MathJax & CSP for Jekyll
|
74
|
+
test_files: []
|