From 22589cc068cb215ca5e19e59023ebaca17d8acdc Mon Sep 17 00:00:00 2001 From: "Garen J. Torikian" Date: Wed, 29 May 2024 17:37:57 -0500 Subject: [PATCH 1/2] Support plugins when processing node tree --- ext/commonmarker/src/lib.rs | 142 ++++-------------- ext/commonmarker/src/node.rs | 71 ++++++--- ext/commonmarker/src/plugins.rs | 3 + .../src/plugins/syntax_highlighting.rs | 106 ++++++++++++- test/node_test.rb | 20 ++- 5 files changed, 202 insertions(+), 140 deletions(-) diff --git a/ext/commonmarker/src/lib.rs b/ext/commonmarker/src/lib.rs index b08e113e..f0374eb4 100644 --- a/ext/commonmarker/src/lib.rs +++ b/ext/commonmarker/src/lib.rs @@ -1,27 +1,15 @@ extern crate core; -use std::path::PathBuf; - -use ::syntect::highlighting::ThemeSet; -use comrak::{ - adapters::SyntaxHighlighterAdapter, - markdown_to_html, markdown_to_html_with_plugins, parse_document, - plugins::syntect::{SyntectAdapter, SyntectAdapterBuilder}, - ComrakOptions, ComrakPlugins, -}; -use magnus::{ - define_module, exception, function, r_hash::ForEach, scan_args, Error, RHash, Symbol, Value, -}; +use comrak::{markdown_to_html_with_plugins, parse_document, ComrakOptions}; +use magnus::{define_module, function, r_hash::ForEach, scan_args, Error, RHash, Symbol, Value}; use node::CommonmarkerNode; +use plugins::syntax_highlighting::construct_syntax_highlighter_from_plugin; mod options; use options::iterate_options_hash; mod plugins; -use plugins::{ - syntax_highlighting::{fetch_syntax_highlighter_path, fetch_syntax_highlighter_theme}, - SYNTAX_HIGHLIGHTER_PLUGIN, -}; + use typed_arena::Arena; mod node; @@ -63,6 +51,32 @@ fn commonmark_to_html(args: &[Value]) -> Result { )?; let (rb_options, rb_plugins) = kwargs.optional; + let comrak_options = match format_options(rb_options) { + Ok(options) => options, + Err(err) => return Err(err), + }; + + let mut comrak_plugins = comrak::Plugins::default(); + + let syntect_adapter = match construct_syntax_highlighter_from_plugin(rb_plugins) { + Ok(Some(adapter)) => Some(adapter), + Ok(None) => None, + Err(err) => return Err(err), + }; + + match syntect_adapter { + Some(ref adapter) => comrak_plugins.render.codefence_syntax_highlighter = Some(adapter), + None => comrak_plugins.render.codefence_syntax_highlighter = None, + } + + Ok(markdown_to_html_with_plugins( + &rb_commonmark, + &comrak_options, + &comrak_plugins, + )) +} + +fn format_options(rb_options: Option) -> Result { let mut comrak_options = ComrakOptions::default(); if let Some(rb_options) = rb_options { @@ -72,101 +86,7 @@ fn commonmark_to_html(args: &[Value]) -> Result { })?; } - if let Some(rb_plugins) = rb_plugins { - let mut comrak_plugins = ComrakPlugins::default(); - - let syntax_highlighter: Option<&dyn SyntaxHighlighterAdapter>; - let adapter: SyntectAdapter; - - let theme = match rb_plugins.get(Symbol::new(SYNTAX_HIGHLIGHTER_PLUGIN)) { - Some(syntax_highlighter_options) => { - match fetch_syntax_highlighter_theme(syntax_highlighter_options) { - Ok(theme) => theme, - Err(e) => { - return Err(e); - } - } - } - None => None, // no `syntax_highlighter:` defined - }; - - match theme { - None => syntax_highlighter = None, - Some(theme) => { - if theme.is_empty() { - // no theme? uss css classes - adapter = SyntectAdapter::new(None); - syntax_highlighter = Some(&adapter); - } else { - let path = match rb_plugins.get(Symbol::new(SYNTAX_HIGHLIGHTER_PLUGIN)) { - Some(syntax_highlighter_options) => { - fetch_syntax_highlighter_path(syntax_highlighter_options)? - } - None => PathBuf::from("".to_string()), // no `syntax_highlighter:` defined - }; - - if path.exists() { - if !path.is_dir() { - return Err(Error::new( - exception::arg_error(), - "`path` needs to be a directory", - )); - } - - let builder = SyntectAdapterBuilder::new(); - let mut ts = ThemeSet::load_defaults(); - - match ts.add_from_folder(&path) { - Ok(_) => {} - Err(e) => { - return Err(Error::new( - exception::arg_error(), - format!("failed to load theme set from path: {e}"), - )); - } - } - - // check if the theme exists in the dir - match ts.themes.get(&theme) { - Some(theme) => theme, - None => { - return Err(Error::new( - exception::arg_error(), - format!("theme `{}` does not exist", theme), - )); - } - }; - - adapter = builder.theme_set(ts).theme(&theme).build(); - - syntax_highlighter = Some(&adapter); - } else { - // no path? default theme lookup - ThemeSet::load_defaults() - .themes - .get(&theme) - .ok_or_else(|| { - Error::new( - exception::arg_error(), - format!("theme `{}` does not exist", theme), - ) - })?; - adapter = SyntectAdapter::new(Some(&theme)); - syntax_highlighter = Some(&adapter); - } - } - } - } - comrak_plugins.render.codefence_syntax_highlighter = syntax_highlighter; - - Ok(markdown_to_html_with_plugins( - &rb_commonmark, - &comrak_options, - &comrak_plugins, - )) - } else { - Ok(markdown_to_html(&rb_commonmark, &comrak_options)) - } + Ok(comrak_options) } #[magnus::init] diff --git a/ext/commonmarker/src/node.rs b/ext/commonmarker/src/node.rs index c4712e40..0393916a 100644 --- a/ext/commonmarker/src/node.rs +++ b/ext/commonmarker/src/node.rs @@ -1,20 +1,20 @@ +use comrak::arena_tree::Node as ComrakNode; use comrak::nodes::{ Ast as ComrakAst, AstNode as ComrakAstNode, ListDelimType, ListType, NodeCode, NodeCodeBlock, NodeDescriptionItem, NodeFootnoteDefinition, NodeFootnoteReference, NodeHeading, NodeHtmlBlock, NodeLink, NodeList, NodeMath, NodeMultilineBlockQuote, NodeShortCode, NodeTable, NodeValue as ComrakNodeValue, TableAlignment, }; -use comrak::{arena_tree::Node as ComrakNode, ComrakOptions}; use magnus::RArray; -use magnus::{ - function, method, r_hash::ForEach, scan_args, Module, Object, RHash, RModule, Symbol, Value, -}; +use magnus::{function, method, scan_args, Module, Object, RHash, RModule, Symbol, Value}; use rctree::Node; use typed_arena::Arena; use std::cell::RefCell; -use crate::options::iterate_options_hash; +use crate::format_options; + +use crate::plugins::syntax_highlighting::construct_syntax_highlighter_from_plugin; #[derive(Debug, Clone)] #[magnus::wrap(class = "Commonmarker::Node::Ast", size, mark)] @@ -895,15 +895,24 @@ impl CommonmarkerNode { &[], &["options", "plugins"], )?; - let (rb_options, _rb_plugins) = kwargs.optional; + let (rb_options, rb_plugins) = kwargs.optional; + + let comrak_options = match format_options(rb_options) { + Ok(options) => options, + Err(err) => return Err(err), + }; + + let mut comrak_plugins = comrak::Plugins::default(); - let mut comrak_options = ComrakOptions::default(); + let syntect_adapter = match construct_syntax_highlighter_from_plugin(rb_plugins) { + Ok(Some(adapter)) => Some(adapter), + Ok(None) => None, + Err(err) => return Err(err), + }; - if let Some(rb_options) = rb_options { - rb_options.foreach(|key: Symbol, value: RHash| { - iterate_options_hash(&mut comrak_options, key, value)?; - Ok(ForEach::Continue) - })?; + match syntect_adapter { + Some(ref adapter) => comrak_plugins.render.codefence_syntax_highlighter = Some(adapter), + None => comrak_plugins.render.codefence_syntax_highlighter = None, } let arena: Arena = Arena::new(); @@ -936,7 +945,12 @@ impl CommonmarkerNode { } let mut output = vec![]; - match comrak::format_html(&comrak_root_node, &comrak_options, &mut output) { + match comrak::format_html_with_plugins( + &comrak_root_node, + &comrak_options, + &mut output, + &comrak_plugins, + ) { Ok(_) => {} Err(e) => { return Err(magnus::Error::new( @@ -963,15 +977,25 @@ impl CommonmarkerNode { &[], &["options", "plugins"], )?; - let (rb_options, _rb_plugins) = kwargs.optional; + let (rb_options, rb_plugins) = kwargs.optional; + + let _comrak_options = format_options(rb_options); + let comrak_options = match format_options(rb_options) { + Ok(options) => options, + Err(err) => return Err(err), + }; - let mut comrak_options = ComrakOptions::default(); + let mut comrak_plugins = comrak::Plugins::default(); + + let syntect_adapter = match construct_syntax_highlighter_from_plugin(rb_plugins) { + Ok(Some(adapter)) => Some(adapter), + Ok(None) => None, + Err(err) => return Err(err), + }; - if let Some(rb_options) = rb_options { - rb_options.foreach(|key: Symbol, value: RHash| { - iterate_options_hash(&mut comrak_options, key, value)?; - Ok(ForEach::Continue) - })?; + match syntect_adapter { + Some(ref adapter) => comrak_plugins.render.codefence_syntax_highlighter = Some(adapter), + None => comrak_plugins.render.codefence_syntax_highlighter = None, } let arena: Arena = Arena::new(); @@ -1004,7 +1028,12 @@ impl CommonmarkerNode { } let mut output = vec![]; - match comrak::format_commonmark(&comrak_root_node, &comrak_options, &mut output) { + match comrak::format_commonmark_with_plugins( + &comrak_root_node, + &comrak_options, + &mut output, + &comrak_plugins, + ) { Ok(_) => {} Err(e) => { return Err(magnus::Error::new( diff --git a/ext/commonmarker/src/plugins.rs b/ext/commonmarker/src/plugins.rs index 4587dc9b..c1ea439a 100644 --- a/ext/commonmarker/src/plugins.rs +++ b/ext/commonmarker/src/plugins.rs @@ -1,3 +1,6 @@ pub mod syntax_highlighting; pub const SYNTAX_HIGHLIGHTER_PLUGIN: &str = "syntax_highlighter"; + +pub const SYNTAX_HIGHLIGHTER_PLUGIN_THEME_KEY: &str = "theme"; +pub const SYNTAX_HIGHLIGHTER_PLUGIN_PATH_KEY: &str = "path"; diff --git a/ext/commonmarker/src/plugins/syntax_highlighting.rs b/ext/commonmarker/src/plugins/syntax_highlighting.rs index 6e466d20..836158d8 100644 --- a/ext/commonmarker/src/plugins/syntax_highlighting.rs +++ b/ext/commonmarker/src/plugins/syntax_highlighting.rs @@ -1,14 +1,106 @@ use std::path::PathBuf; +use comrak::plugins::syntect::{SyntectAdapter, SyntectAdapterBuilder}; + use magnus::value::ReprValue; -use magnus::{RHash, Symbol, TryConvert, Value}; +use magnus::{exception, RHash, Symbol, TryConvert, Value}; +use syntect::highlighting::ThemeSet; use crate::EMPTY_STR; -pub const SYNTAX_HIGHLIGHTER_PLUGIN_THEME_KEY: &str = "theme"; -pub const SYNTAX_HIGHLIGHTER_PLUGIN_PATH_KEY: &str = "path"; +pub fn construct_syntax_highlighter_from_plugin( + rb_plugins: Option, +) -> Result, magnus::Error> { + match rb_plugins { + None => Ok(None), + Some(rb_plugins) => { + let theme = match rb_plugins.get(Symbol::new(super::SYNTAX_HIGHLIGHTER_PLUGIN)) { + Some(syntax_highlighter_options) => { + match fetch_syntax_highlighter_theme(syntax_highlighter_options) { + Ok(theme) => theme, + Err(e) => { + return Err(e); + } + } + } + None => None, // no `syntax_highlighter:` defined + }; + + let adapter: SyntectAdapter; + + match theme { + None => Ok(None), + Some(theme) => { + if theme.is_empty() { + // no theme? uss css classes + adapter = SyntectAdapter::new(None); + Ok(Some(adapter)) + } else { + let path = + match rb_plugins.get(Symbol::new(super::SYNTAX_HIGHLIGHTER_PLUGIN)) { + Some(syntax_highlighter_options) => { + fetch_syntax_highlighter_path(syntax_highlighter_options)? + } + None => PathBuf::from("".to_string()), // no `syntax_highlighter:` defined + }; + + if path.exists() { + if !path.is_dir() { + return Err(magnus::Error::new( + exception::arg_error(), + "`path` needs to be a directory", + )); + } + + let builder = SyntectAdapterBuilder::new(); + let mut ts = ThemeSet::load_defaults(); + + match ts.add_from_folder(&path) { + Ok(_) => {} + Err(e) => { + return Err(magnus::Error::new( + exception::arg_error(), + format!("failed to load theme set from path: {e}"), + )); + } + } + + // check if the theme exists in the dir + match ts.themes.get(&theme) { + Some(theme) => theme, + None => { + return Err(magnus::Error::new( + exception::arg_error(), + format!("theme `{}` does not exist", theme), + )); + } + }; + + adapter = builder.theme_set(ts).theme(&theme).build(); + + Ok(Some(adapter)) + } else { + // no path? default theme lookup + ThemeSet::load_defaults() + .themes + .get(&theme) + .ok_or_else(|| { + magnus::Error::new( + exception::arg_error(), + format!("theme `{}` does not exist", theme), + ) + })?; + adapter = SyntectAdapter::new(Some(&theme)); + Ok(Some(adapter)) + } + } + } + } + } + } +} -pub fn fetch_syntax_highlighter_theme(value: Value) -> Result, magnus::Error> { +fn fetch_syntax_highlighter_theme(value: Value) -> Result, magnus::Error> { if value.is_nil() { // `syntax_highlighter: nil` return Ok(None); @@ -29,7 +121,7 @@ pub fn fetch_syntax_highlighter_theme(value: Value) -> Result, ma )); } - let theme_key = Symbol::new(SYNTAX_HIGHLIGHTER_PLUGIN_THEME_KEY); + let theme_key = Symbol::new(super::SYNTAX_HIGHLIGHTER_PLUGIN_THEME_KEY); match syntax_highlighter_plugin.get(theme_key) { Some(theme) => { @@ -48,14 +140,14 @@ pub fn fetch_syntax_highlighter_theme(value: Value) -> Result, ma } } -pub fn fetch_syntax_highlighter_path(value: Value) -> Result { +fn fetch_syntax_highlighter_path(value: Value) -> Result { if value.is_nil() { // `syntax_highlighter: nil` return Ok(PathBuf::from(EMPTY_STR)); } let syntax_highlighter_plugin: RHash = TryConvert::try_convert(value)?; - let path_key = Symbol::new(SYNTAX_HIGHLIGHTER_PLUGIN_PATH_KEY); + let path_key = Symbol::new(super::SYNTAX_HIGHLIGHTER_PLUGIN_PATH_KEY); match syntax_highlighter_plugin.get(path_key) { Some(path) => { diff --git a/test/node_test.rb b/test/node_test.rb index 3b9bbd4f..188637af 100644 --- a/test/node_test.rb +++ b/test/node_test.rb @@ -100,6 +100,22 @@ def test_delete assert_match(%r{

Hi . This has many nodes!

\n}, @document.to_html) end + def test_node_html_with_plugins + code = <<~CODE + ```ruby + puts "hello" + ``` + CODE + + plugins = { syntax_highlighter: { theme: "InspiredGitHub" } } + + result = Commonmarker.to_html(code, plugins: plugins) + + doc = Commonmarker.parse(code) + + assert_equal(result, doc.to_html(plugins: plugins)) + end + class StringContentTest < Minitest::Test def setup @document = Commonmarker.parse("**HELLO!** \n***\n This has `nodes`!") @@ -272,10 +288,12 @@ def test_has_fence_info end def test_can_set_fence_info + assert_match(/
puts 'wow'\n<\/code><\/pre>}, @document.to_html)
+      assert_match(/
Date: Sun, 2 Jun 2024 14:32:29 -0500
Subject: [PATCH 2/2] :gem: bump 1.1.4

---
 lib/commonmarker/version.rb | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/lib/commonmarker/version.rb b/lib/commonmarker/version.rb
index 78ac5c4a..8af00e50 100644
--- a/lib/commonmarker/version.rb
+++ b/lib/commonmarker/version.rb
@@ -1,5 +1,5 @@
 # frozen_string_literal: true
 
 module Commonmarker
-  VERSION = "1.1.3"
+  VERSION = "1.1.4"
 end