gjtorikian / commonmarker

Ruby wrapper for the comrak (CommonMark parser) Rust crate
MIT License
416 stars 80 forks source link

Tagging "tasklist" lists with a class for easy styling? #137

Closed nesquena closed 3 years ago

nesquena commented 3 years ago

Hi all! With tasklist extension enabled, this markdown:

- [ ] foo
- [x] bar

Generates the following HTML:

<ul>
    <li><input type="checkbox" disabled="">foo</li>
    <li><input type="checkbox" disabled="" checked="">bar</li>
</ul>

Is it possible to add classes (as GitHub does) to make these style-able?

GitHub renders as such:

<ul class="contains-task-list">
  <li class="task-list-item"><input type="checkbox" id="" disabled="" class="task-list-item-checkbox"> foo</li>
  <li class="task-list-item"><input type="checkbox" id="" disabled="" class="task-list-item-checkbox" checked=""> bar</li>
</ul> 

Why is this feature necessary?

I want to add "list-style: none;" to the <ul> class, as it currently doesn't look good:

image

and I'd want to see this rendered as:

image

Apologies if I've missed a way to do this, I've looked through the docs but couldn't see a way.

kivikakk commented 3 years ago

There's no straightforward way to do this using commonmarker itself; the recommendation is to do as GitHub do and use something like html-pipeline to post-process the generated HTML in stages to apply transforms as needed. This is a fair bit cleaner than trying to do everything in the Markdown→HTML step itself, and offers a lot of flexibility.

You'd need to define a filter that searches for ul li input[type=checkbox] and mark the <ul>s and <li>s appropriately. An alternative would be to create your own tasklist transformer, which is how GitHub do it themselves — they don't actually use the tasklist extension provided in cmark-gfm (and thus commonmarker).

kivikakk commented 3 years ago

Oh, what am I saying — you can of course subclass Renderer. See how the provided HtmlRenderer renders tasklist items.

nesquena commented 3 years ago

Thanks @kivikakk, good to know, I'll look at that and see if I can take the renderer subclass approach then. If I figure out the best way to modify, I am assuming I'd need to determine if a node is a tasklist and then use that to override list_item https://github.com/gjtorikian/commonmarker/blob/b4335a63fe177160788d31b5325e1a0fc624a8c9/lib/commonmarker/renderer/html_renderer.rb#L57-L64 since tasklist seems to only have control of the "input" not the li or ul. I'll respond back here if I figure things out. If anyone else has done this before in another project, would be interested to hear if this seems like the best path forward and if you have any prior work on this. Good to know about the html-pipeline option as a fallback.

Maybe something like:

def list_item(node) 
   block do 
     tasklist_data = tasklist(node) 
     container("<li#{" class='task-list-item'" if tasklist?(node)}#{sourcepos(node)}#{tasklist_data}>#{' ' if tasklist?(node)}", '</li>') do 
       out(:children) 
     end 
   end 
 end 

I am happy for this to be closed for now, although I would wonder since 9/10 times you don't want to render tasklists with a bullet if it wouldn't be worth making this easier to achieve in the future. I understand that this may also be partly a responsibility of the underlying commonmark library though to expose that or simply out of scope.

nesquena commented 3 years ago

Update, I ended up going the html-pipeline approach, here's what I did for anyone else that stumbles upon this later. Setup html-pipeline:

gem 'html-pipeline', '~> 2.14'

Created a custom filter to add the classes to the ul and li:

class MarkdownTaskListClassFilter < HTML::Pipeline::Filter
  def call
    doc.search("ul > li > input[type=checkbox]").each do |input|
      input.parent['class'] = "task-list-item" # li
      input.parent.parent['class'] = "contains-task-list" # ul
    end
    doc
  end
end

Process the filter in my code here after rendering the markdown to HTML:

# Render the body to HTML to `html_body` using commonmark above... ​
pipeline = HTML::Pipeline.new [MarkdownTaskListClassFilter]
result = pipeline.call html_body
result[:output].to_s

I'm all set with that solution, thanks again.