jekyll-mathjax-csp 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (4) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +70 -0
  3. data/lib/jekyll-mathjax-csp.rb +217 -0
  4. 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: []