11//! Syntax highlighting for code blocks
2+ use anyhow:: anyhow;
23use syntect:: highlighting:: ThemeSet ;
34use syntect:: html:: highlighted_html_for_string;
45use syntect:: parsing:: SyntaxSet ;
@@ -7,7 +8,7 @@ use crate::parser::core::CoreRule;
78use crate :: parser:: extset:: MarkdownItExt ;
89use crate :: plugins:: cmark:: block:: code:: CodeBlock ;
910use crate :: plugins:: cmark:: block:: fence:: CodeFence ;
10- use crate :: { MarkdownIt , Node , NodeValue , Renderer } ;
11+ use crate :: { MarkdownIt , Node , NodeValue , Renderer , Result } ;
1112
1213#[ derive( Debug ) ]
1314pub struct SyntectSnippet {
@@ -40,7 +41,18 @@ pub fn set_theme(md: &mut MarkdownIt, theme: &'static str) {
4041
4142pub struct SyntectRule ;
4243impl CoreRule for SyntectRule {
44+ fn try_run ( root : & mut Node , md : & MarkdownIt ) -> Result < ( ) > {
45+ Self :: _run :: < true > ( root, md) ?;
46+ Ok ( ( ) )
47+ }
48+
4349 fn run ( root : & mut Node , md : & MarkdownIt ) {
50+ let _ = Self :: _run :: < false > ( root, md) ;
51+ }
52+ }
53+
54+ impl SyntectRule {
55+ fn _run < const CAN_FAIL : bool > ( root : & mut Node , md : & MarkdownIt ) -> Result < ( ) > {
4456 let ss = SyntaxSet :: load_defaults_newlines ( ) ;
4557 let ts = ThemeSet :: load_defaults ( ) ;
4658 let theme = & ts. themes [ md. ext . get :: < SyntectSettings > ( ) . copied ( ) . unwrap_or_default ( ) . 0 ] ;
@@ -59,17 +71,135 @@ impl CoreRule for SyntectRule {
5971 if let Some ( content) = content {
6072 let mut syntax = None ;
6173 if let Some ( language) = language {
62- syntax = ss. find_syntax_by_token ( & language) ;
74+ let language = language. trim ( ) ;
75+ if !language. is_empty ( ) {
76+ syntax = ss. find_syntax_by_token ( language) ;
77+
78+ if CAN_FAIL && syntax. is_none ( ) {
79+ return Err ( anyhow ! ( "syntax not found for language `{language}`" ) ) ;
80+ }
81+ }
6382 }
64- let syntax = syntax. unwrap_or_else ( || ss. find_syntax_plain_text ( ) ) ;
6583
84+ let syntax = syntax. unwrap_or_else ( || ss. find_syntax_plain_text ( ) ) ;
6685 let html = highlighted_html_for_string ( content, & ss, syntax, theme) ;
6786
6887 if let Ok ( html) = html {
6988 node. replace ( SyntectSnippet { html } ) ;
7089 }
7190 }
7291 Ok ( ( ) )
92+ } )
93+ }
94+ }
95+
96+ #[ cfg( test) ]
97+ mod tests {
98+ use indoc:: { indoc, formatdoc} ;
99+
100+ fn run ( input : & str ) -> String {
101+ let md = & mut crate :: MarkdownIt :: new ( ) ;
102+ crate :: plugins:: cmark:: block:: fence:: add ( md) ;
103+ crate :: plugins:: extra:: syntect:: add ( md) ;
104+ let node = md. parse ( & ( input. to_owned ( ) + "\n " ) ) ;
105+ node. walk ( |node, _| {
106+ assert ! ( node. srcmap. is_some( ) ) ;
107+ Ok ( ( ) )
108+ } ) . unwrap ( ) ;
109+ node. render ( )
110+ }
111+
112+ fn try_run ( input : & str ) -> crate :: Result < String > {
113+ let md = & mut crate :: MarkdownIt :: new ( ) ;
114+ crate :: plugins:: cmark:: block:: fence:: add ( md) ;
115+ crate :: plugins:: extra:: syntect:: add ( md) ;
116+ let node = md. try_parse ( & ( input. to_owned ( ) + "\n " ) ) ?;
117+ node. walk ( |node, _| {
118+ assert ! ( node. srcmap. is_some( ) ) ;
119+ Ok ( ( ) )
73120 } ) . unwrap ( ) ;
121+ Ok ( node. render ( ) )
122+ }
123+
124+ #[ test]
125+ fn no_lang_prefix ( ) {
126+ let input = indoc ! ( r#"
127+ ```
128+ hello
129+ ```
130+ "# ) ;
131+
132+ let output = indoc ! ( r#"
133+ <pre style="background-color:#ffffff;">
134+ <span style="color:#323232;">hello
135+ </span></pre>
136+ "# ) ;
137+
138+ assert_eq ! ( run( input) , output) ;
139+ assert_eq ! ( try_run( input) . ok( ) . unwrap( ) , output) ;
140+ }
141+
142+ #[ test]
143+ fn rust_highlight ( ) {
144+ let input = indoc ! ( r#"
145+ ```rust
146+ let hello = "world";
147+ ```
148+ "# ) ;
149+
150+ let output = indoc ! ( r#"
151+ <pre style="background-color:#ffffff;">
152+ <span style="font-weight:bold;color:#a71d5d;">let</span>
153+ <span style="color:#323232;"> hello </span>
154+ <span style="font-weight:bold;color:#a71d5d;">= </span>
155+ <span style="color:#183691;">"world"</span>
156+ <span style="color:#323232;">;</span>
157+ </pre>
158+ "# ) ;
159+
160+ assert_eq ! ( run( input) . replace( '\n' , "" ) , output. replace( '\n' , "" ) ) ;
161+ assert_eq ! ( try_run( input) . ok( ) . unwrap( ) . replace( '\n' , "" ) , output. replace( '\n' , "" ) ) ;
162+ }
163+
164+ #[ test]
165+ fn rust_highlight_trim_spaces ( ) {
166+ let input = & formatdoc ! ( r#"
167+ ``` rust{}
168+ let hello = "world";
169+ ```
170+ "# , " " ) ;
171+
172+ let output = indoc ! ( r#"
173+ <pre style="background-color:#ffffff;">
174+ <span style="font-weight:bold;color:#a71d5d;">let</span>
175+ <span style="color:#323232;"> hello </span>
176+ <span style="font-weight:bold;color:#a71d5d;">= </span>
177+ <span style="color:#183691;">"world"</span>
178+ <span style="color:#323232;">;</span>
179+ </pre>
180+ "# ) ;
181+
182+ assert_eq ! ( run( input) . replace( '\n' , "" ) , output. replace( '\n' , "" ) ) ;
183+ assert_eq ! ( try_run( input) . ok( ) . unwrap( ) . replace( '\n' , "" ) , output. replace( '\n' , "" ) ) ;
184+ }
185+
186+ #[ test]
187+ fn unknown_lang ( ) {
188+ let input = indoc ! ( r#"
189+ ```some-unknown-language
190+ hello
191+ ```
192+ "# ) ;
193+
194+ let output = indoc ! ( r#"
195+ <pre style="background-color:#ffffff;">
196+ <span style="color:#323232;">hello
197+ </span></pre>
198+ "# ) ;
199+
200+ assert_eq ! ( run( input) , output) ;
201+ assert ! (
202+ format!( "{:?}" , try_run( input) . err( ) . unwrap( ) ) . contains( "syntax not found for language" )
203+ ) ;
74204 }
75205}
0 commit comments