jekyll-mathjax-csp 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|