@@ -47,6 +47,11 @@ const CODE_INDENT: usize = 4;
4747// be nested this deeply.
4848const MAX_LIST_DEPTH : usize = 100 ;
4949
50+ /// Shorthand for checking if a node's value matches the given expression.
51+ ///
52+ /// Note this will `borrow()` the provided node's data attribute while doing the
53+ /// check, which will fail if the node is already mutably borrowed.
54+ #[ macro_export]
5055macro_rules! node_matches {
5156 ( $node: expr, $( $pat: pat ) |+) => { {
5257 matches!(
@@ -731,6 +736,25 @@ pub struct ParseOptions<'c> {
731736 #[ cfg_attr( feature = "bon" , builder( default ) ) ]
732737 pub relaxed_tasklist_matching : bool ,
733738
739+ /// Whether tasklist items can be parsed in table cells. At present, the
740+ /// tasklist item must be the only content in the cell. Both tables and
741+ /// tasklists much be enabled for this to work.
742+ ///
743+ /// ```
744+ /// # use comrak::{markdown_to_html, Options};
745+ /// let mut options = Options::default();
746+ /// options.extension.table = true;
747+ /// options.extension.tasklist = true;
748+ /// assert_eq!(markdown_to_html("| val |\n| - |\n| [ ] |\n", &options),
749+ /// "<table>\n<thead>\n<tr>\n<th>val</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>[ ]</td>\n</tr>\n</tbody>\n</table>\n");
750+ ///
751+ /// options.parse.tasklist_in_table = true;
752+ /// assert_eq!(markdown_to_html("| val |\n| - |\n| [ ] |\n", &options),
753+ /// "<table>\n<thead>\n<tr>\n<th>val</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>\n<input type=\"checkbox\" disabled=\"\" /> </td>\n</tr>\n</tbody>\n</table>\n");
754+ /// ```
755+ #[ cfg_attr( feature = "bon" , builder( default ) ) ]
756+ pub tasklist_in_table : bool ,
757+
734758 /// Relax parsing of autolinks, allow links to be detected inside brackets
735759 /// and allow all url schemes. It is intended to allow a very specific type of autolink
736760 /// detection, such as `[this http://and.com that]` or `{http://foo.com}`, on a best can basis.
@@ -3053,6 +3077,13 @@ where
30533077 }
30543078 }
30553079
3080+ // Processes tasklist items in a text node. This function
3081+ // must not detach `node`, as we iterate through siblings in
3082+ // `postprocess_text_nodes_with_context` and may end up relying on it
3083+ // remaining in place.
3084+ //
3085+ // `text` is the mutably borrowed textual content of `node`. If it is empty
3086+ // after the call to `process_tasklist`, it will be properly cleaned up.
30563087 fn process_tasklist (
30573088 & mut self ,
30583089 node : & ' a AstNode < ' a > ,
@@ -3072,49 +3103,73 @@ where
30723103 }
30733104
30743105 let parent = node. parent ( ) . unwrap ( ) ;
3075- if node. previous_sibling ( ) . is_some ( ) || parent. previous_sibling ( ) . is_some ( ) {
3076- return ;
3077- }
30783106
3079- if !node_matches ! ( parent, NodeValue :: Paragraph ) {
3080- return ;
3081- }
3107+ if node_matches ! ( parent, NodeValue :: TableCell ) {
3108+ if !self . options . parse . tasklist_in_table {
3109+ return ;
3110+ }
30823111
3083- let grandparent = parent. parent ( ) . unwrap ( ) ;
3084- if !node_matches ! ( grandparent, NodeValue :: Item ( ..) ) {
3085- return ;
3086- }
3112+ if node. previous_sibling ( ) . is_some ( ) || node. next_sibling ( ) . is_some ( ) {
3113+ return ;
3114+ }
30873115
3088- let great_grandparent = grandparent. parent ( ) . unwrap ( ) ;
3089- if !node_matches ! ( great_grandparent, NodeValue :: List ( ..) ) {
3090- return ;
3091- }
3116+ // For now, require the task item is the only content of the table cell.
3117+ // If we want to relax this later, we can.
3118+ if end != text. len ( ) {
3119+ return ;
3120+ }
30923121
3093- // These are sound only because the exact text that we've matched and
3094- // the count thereof (i.e. "end") will precisely map to characters in
3095- // the source document.
3096- text. drain ( ..end) ;
3122+ text. drain ( ..end) ;
3123+ parent. prepend (
3124+ self . arena . alloc (
3125+ Ast :: new_with_sourcepos (
3126+ NodeValue :: TaskItem ( if symbol == ' ' { None } else { Some ( symbol) } ) ,
3127+ * sourcepos,
3128+ )
3129+ . into ( ) ,
3130+ ) ,
3131+ ) ;
3132+ } else if node_matches ! ( parent, NodeValue :: Paragraph ) {
3133+ if node. previous_sibling ( ) . is_some ( ) || parent. previous_sibling ( ) . is_some ( ) {
3134+ return ;
3135+ }
30973136
3098- let adjust = spx. consume ( end) + 1 ;
3099- assert_eq ! (
3100- sourcepos. start. column,
3101- parent. data. borrow( ) . sourcepos. start. column
3102- ) ;
3137+ let grandparent = parent. parent ( ) . unwrap ( ) ;
3138+ if !node_matches ! ( grandparent, NodeValue :: Item ( ..) ) {
3139+ return ;
3140+ }
31033141
3104- // See tests::fuzz::echaw9. The paragraph doesn't exist in the source,
3105- // so we remove it.
3106- if sourcepos. end . column < adjust && node. next_sibling ( ) . is_none ( ) {
3107- parent. detach ( ) ;
3108- } else {
3109- sourcepos. start . column = adjust;
3110- parent. data . borrow_mut ( ) . sourcepos . start . column = adjust;
3111- }
3142+ let great_grandparent = grandparent. parent ( ) . unwrap ( ) ;
3143+ if !node_matches ! ( great_grandparent, NodeValue :: List ( ..) ) {
3144+ return ;
3145+ }
31123146
3113- grandparent. data . borrow_mut ( ) . value =
3114- NodeValue :: TaskItem ( if symbol == ' ' { None } else { Some ( symbol) } ) ;
3147+ // These are sound only because the exact text that we've matched and
3148+ // the count thereof (i.e. "end") will precisely map to characters in
3149+ // the source document.
3150+ text. drain ( ..end) ;
31153151
3116- if let NodeValue :: List ( ref mut list) = & mut great_grandparent. data . borrow_mut ( ) . value {
3117- list. is_task_list = true ;
3152+ let adjust = spx. consume ( end) + 1 ;
3153+ assert_eq ! (
3154+ sourcepos. start. column,
3155+ parent. data. borrow( ) . sourcepos. start. column
3156+ ) ;
3157+
3158+ // See tests::fuzz::echaw9. The paragraph doesn't exist in the source,
3159+ // so we remove it.
3160+ if sourcepos. end . column < adjust && node. next_sibling ( ) . is_none ( ) {
3161+ parent. detach ( ) ;
3162+ } else {
3163+ sourcepos. start . column = adjust;
3164+ parent. data . borrow_mut ( ) . sourcepos . start . column = adjust;
3165+ }
3166+
3167+ grandparent. data . borrow_mut ( ) . value =
3168+ NodeValue :: TaskItem ( if symbol == ' ' { None } else { Some ( symbol) } ) ;
3169+
3170+ if let NodeValue :: List ( ref mut list) = & mut great_grandparent. data . borrow_mut ( ) . value {
3171+ list. is_task_list = true ;
3172+ }
31183173 }
31193174 }
31203175
0 commit comments