From 8e4905522ddf57dba2da336a0192962104731db2 Mon Sep 17 00:00:00 2001 From: Mac Siri Date: Mon, 9 Mar 2020 17:58:54 -0400 Subject: [PATCH] Implement KatexTag (#6237) [deploy] * Install Katex gem * Add Katex stylesheet and spec * Implement basic error handling * Remove comment * Create more spec * Update approval file Changes is due to turning off RedCarpet's superscript feature * Add instruction to _editor_guide_text * Serve our own KaTeX css * Apply Rhyme's suggestion * Limit Katex's css use * Update spec * Add spec Co-authored-by: Ben Halpern --- Gemfile | 1 + Gemfile.lock | 3 ++ app/assets/config/manifest.js | 1 + app/assets/stylesheets/ltags/KatexTag.scss | 1 + app/liquid_tags/katex_tag.rb | 31 ++++++++++++++++ app/views/liquids/_katex.html.erb | 7 ++++ app/views/pages/_editor_guide_text.html.erb | 5 +++ config/initializers/redcarpet.rb | 2 +- .../generates_katex_output.approved.html | 12 +++++++ spec/liquid_tags/katex_tag_spec.rb | 36 +++++++++++++++++++ .../user_preview_article_body.approved.html | 3 +- 11 files changed, 100 insertions(+), 2 deletions(-) create mode 100644 app/assets/stylesheets/ltags/KatexTag.scss create mode 100644 app/liquid_tags/katex_tag.rb create mode 100644 app/views/liquids/_katex.html.erb create mode 100644 spec/fixtures/approvals/katextag/render/generates_katex_output.approved.html create mode 100644 spec/liquid_tags/katex_tag_spec.rb diff --git a/Gemfile b/Gemfile index 11b2b7959..64d71a4d1 100644 --- a/Gemfile +++ b/Gemfile @@ -58,6 +58,7 @@ gem "inline_svg", "~> 1.7" # Embed SVG documents in your Rails views and style t gem "jbuilder", "~> 2.10" # Create JSON structures via a Builder-style DSL gem "jquery-rails", "~> 4.3" # A gem to automate using jQuery with Rails gem "kaminari", "~> 1.2" # A Scope & Engine based, clean, powerful, customizable and sophisticated paginator +gem "katex", "~> 0.6.0" # This rubygem enables you to render TeX math to HTML using KaTeX. It uses ExecJS under the hood gem "libhoney", "~> 1.14" # Ruby gem for sending data to Honeycomb gem "liquid", "~> 4.0" # A secure, non-evaling end user template engine with aesthetic markup gem "nokogiri", "~> 1.10" # HTML, XML, SAX, and Reader parser diff --git a/Gemfile.lock b/Gemfile.lock index abf3cc945..e1cafd6da 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -453,6 +453,8 @@ GEM activerecord kaminari-core (= 1.2.0) kaminari-core (1.2.0) + katex (0.6.0) + execjs (~> 2.7) launchy (2.5.0) addressable (~> 2.7) libhoney (1.14.4) @@ -925,6 +927,7 @@ DEPENDENCIES jbuilder (~> 2.10) jquery-rails (~> 4.3) kaminari (~> 1.2) + katex (~> 0.6.0) launchy (~> 2.5) libhoney (~> 1.14) liquid (~> 4.0) diff --git a/app/assets/config/manifest.js b/app/assets/config/manifest.js index bca3cc0a1..9f69e5ad0 100644 --- a/app/assets/config/manifest.js +++ b/app/assets/config/manifest.js @@ -6,3 +6,4 @@ //= link s3_direct_upload.js // = link administrate/application.css // = link administrate/application.js +// = link katex.css diff --git a/app/assets/stylesheets/ltags/KatexTag.scss b/app/assets/stylesheets/ltags/KatexTag.scss new file mode 100644 index 000000000..725fe286e --- /dev/null +++ b/app/assets/stylesheets/ltags/KatexTag.scss @@ -0,0 +1 @@ +@import 'katex'; diff --git a/app/liquid_tags/katex_tag.rb b/app/liquid_tags/katex_tag.rb new file mode 100644 index 000000000..361c1463e --- /dev/null +++ b/app/liquid_tags/katex_tag.rb @@ -0,0 +1,31 @@ +class KatexTag < Liquid::Block + PARTIAL = "liquids/katex".freeze + KATEX_EXISTED = "katex_existed".freeze + + def initialize(tag_name, markup, tokens) + super + end + + def render(context) + block = Nokogiri::HTML(super).at("body").text + + parsed_content = begin + Katex.render(block, display_mode: true) + rescue ExecJS::ProgramError => e + e.message + end + + should_render_css = !context[KATEX_EXISTED] + + unless context[KATEX_EXISTED] + context[KATEX_EXISTED] = true + end + + ActionController::Base.new.render_to_string( + partial: PARTIAL, + locals: { parsed_content: parsed_content, should_render_css: should_render_css }, + ) + end +end + +Liquid::Template.register_tag("katex", KatexTag) diff --git a/app/views/liquids/_katex.html.erb b/app/views/liquids/_katex.html.erb new file mode 100644 index 000000000..da30faad8 --- /dev/null +++ b/app/views/liquids/_katex.html.erb @@ -0,0 +1,7 @@ +<% if should_render_css %> + <%= stylesheet_link_tag "katex" %> +<% end %> + +
+ <%= parsed_content %> +
diff --git a/app/views/pages/_editor_guide_text.html.erb b/app/views/pages/_editor_guide_text.html.erb index dd4eee9a9..9cb8da7eb 100644 --- a/app/views/pages/_editor_guide_text.html.erb +++ b/app/views/pages/_editor_guide_text.html.erb @@ -201,6 +201,11 @@

RunKit Embed

Put executable code within a runkit liquid block, as follows:

{% runkit
// hidden setup JavaScript code goes in this preamble area
const hiddenVar = 42
%}
// visible, reader-editable JavaScript code goes here
console.log(hiddenVar)
{% endrunkit %}
+ +

KaTeX Embed

+

Place your mathematical expression within a KaTeX liquid block, as follows:

+
{% katex %}
c = \pm\sqrt{a^2 + b^2}
{% endkatex %}
+

Stackblitz Embed

All you need is the ID of the Stackblitz:

{% stackblitz ball-demo %} diff --git a/config/initializers/redcarpet.rb b/config/initializers/redcarpet.rb index 51660959e..09702983a 100644 --- a/config/initializers/redcarpet.rb +++ b/config/initializers/redcarpet.rb @@ -5,7 +5,7 @@ lax_html_blocks: true, lax_spacing: true, strikethrough: true, - superscript: true, + superscript: false, tables: true, footnotes: true }.freeze diff --git a/spec/fixtures/approvals/katextag/render/generates_katex_output.approved.html b/spec/fixtures/approvals/katextag/render/generates_katex_output.approved.html new file mode 100644 index 000000000..d31b7e2de --- /dev/null +++ b/spec/fixtures/approvals/katextag/render/generates_katex_output.approved.html @@ -0,0 +1,12 @@ + + + + + + + +
+ c=±a2+b2c = \pm\sqrt{a^2 + b^2} +
+ + diff --git a/spec/liquid_tags/katex_tag_spec.rb b/spec/liquid_tags/katex_tag_spec.rb new file mode 100644 index 000000000..926f8162d --- /dev/null +++ b/spec/liquid_tags/katex_tag_spec.rb @@ -0,0 +1,36 @@ +require "rails_helper" + +RSpec.describe KatexTag, type: :liquid_tag do + describe "#render" do + def generate_katex_liquid(content) + Liquid::Template.register_tag("katex", described_class) + Liquid::Template.parse("{% katex %}#{content}{% endkatex %}") + end + + it "generates Katex output" do + content = "c = \\pm\\sqrt{a^2 + b^2}" + rendered = generate_katex_liquid(content).render + verify(format: :html) { rendered } + end + + it "includes the css style tag only once when rendering multiple" do + content = <<~CONTENT + {% katex %} + hello + {% endkatex %} + {% katex %} + Hello + {% endkatex %} + CONTENT + Liquid::Template.register_tag("katex", described_class) + render = Liquid::Template.parse(content).render + expect(Nokogiri::HTML(render).css("link").count).to eq(1) + end + + it "generates Katex errors" do + content = "\\c = \\pm\\sqrt{a^2 + b^2}" + rendered = generate_katex_liquid(content).render + expect(rendered).to include("ParseError: KaTeX parse error: ") + end + end +end diff --git a/spec/support/fixtures/approvals/user_preview_article_body.approved.html b/spec/support/fixtures/approvals/user_preview_article_body.approved.html index 998b0771d..1c8bbf4c5 100644 --- a/spec/support/fixtures/approvals/user_preview_article_body.approved.html +++ b/spec/support/fixtures/approvals/user_preview_article_body.approved.html @@ -45,7 +45,8 @@

- multiword heading with ( weird ? punctu&ation

+ multiword heading with ( weird ? punctu&^ation +