Quantcast
Channel: Active questions tagged ruby - Stack Overflow
Viewing all articles
Browse latest Browse all 4634

Simple Table of Contents in Rails is not working as intended

$
0
0

I had this thought of a simple Table of contents in Rails. I have created a new Rails 8 app, worked. I installed action text. Scaffold'ed out a post model with title and body. Body has the rich text and uses the trix editor. Post.rb model has the needed "has_rich_text :body". I went at this in a few different ways but finally landed on just grabbing the body in the model using "before_save" and making adjustments there. The concept is straight forward. When you create a new post, If you add a "heading" (will give you a h1 tag), then I can look through the body, grab those h1 tags, snatch the content, create the anchor tag from the content and modify the h1 to include the id attribute, which would also be the content.

I added the migration for adding "toc" to posts model where toc:rich_text. Ensured "has_rich_text :toc" was in the post.rb model and :toc was added to the strong params. Added the field in the _post.html.erb to show it. Here is my frustration. It works. Once the post is created I get the view with the links that are the heading in the post body. The h1 tags do not have the id attributes. Oh, but if I log it, the before save to body does have the modified code, after save, it is not there, but rather the original post body.

Here is the actions I am working on in the post.rb.

class Post < ApplicationRecord  has_rich_text :body  has_rich_text :toc  # has_rich_text :toc_body  before_save :process_body  private  def process_body    return if body.blank?    # Convert the ActionText::RichText to a string    body_content = body.to_s    toc_data = generate_toc(body_content)    # Log the modified body content    # Rails.logger.debug("Modified Body: #{toc_data[:content]}")    self.body = toc_data[:content]  # Store the modified body    # Log the body after update    # Rails.logger.debug("Body after update: #{self.toc_body}")    self.toc = toc_data[:toc]        # Store the TOC  end  # Method to generate TOC and modify body  def generate_toc(body)    headings = body.scan(/<h1[^>]*>(.*?)<\/h1>/).flatten    return { toc: "", content: body } if headings.empty?    toc = "<ul>"    ids = []  # Array to store generated IDs    headings.each do |heading|      id = heading.gsub(/\s+/, "-").downcase      toc += "<li><a href='##{id}'>#{heading}</a></li>"      ids << { heading: heading, id: id }  # Store heading and corresponding ID    end    # Manually build the new body content    ids.each do |item|      body.gsub!(/<h1>(#{Regexp.escape(item[:heading])})<\/h1>/, "<h1 id='#{item[:id]}'>\\1</h1>")    end    toc += "</ul>"    { toc: toc.html_safe, content: body.html_safe }  endend

Here are the logs, I put bold ** around the points of interest to try and help:

Started POST "/posts" for ::1 at 2024-11-29 15:15:00 -0500Processing by PostsController#create as TURBO_STREAM  Parameters: {"authenticity_token"=>"[FILTERED]", "post"=>{"title"=>"Ruby Blog", "body"=>"<h1>Chapter 1</h1><div>Rails is sunshine and lollipops</div><h1>Chapter 2</h1><div>Not everyone like Brussels Sprouts&nbsp;</div>"}, "commit"=>"Create Post"}  Rendered vendor/bundle/ruby/3.3.0/gems/actiontext-8.0.0/app/views/action_text/contents/_content.html.erb within layouts/action_text/contents/_content (Duration: 4.2ms | GC: 0.0ms)**Modified Body:** <!-- BEGIN app/views/layouts/action_text/contents/_content.html.erb --><div class="trix-content"><!-- BEGIN vendor/bundle/ruby/3.3.0/gems/actiontext-8.0.0/app/views/action_text/contents/_content.html.erb -->**<h1 id='chapter-1'>**Chapter 1</h1><div>Rails is sunshine and lollipops</div>**<h1 id='chapter-2'>**Chapter 2</h1><div>Not everyone like Brussels Sprouts&nbsp;</div><!-- END vendor/bundle/ruby/3.3.0/gems/actiontext-8.0.0/app/views/action_text/contents/_content.html.erb --></div><!-- END app/views/layouts/action_text/contents/_content.html.erb -->  Rendered vendor/bundle/ruby/3.3.0/gems/actiontext-8.0.0/app/views/action_text/contents/_content.html.erb within layouts/action_text/contents/_content (Duration: 2.7ms | GC: 0.0ms)**Body after update:** <!-- BEGIN app/views/layouts/action_text/contents/_content.html.erb --><div class="trix-content"><!-- BEGIN vendor/bundle/ruby/3.3.0/gems/actiontext-8.0.0/app/views/action_text/contents/_content.html.erb --><div class="trix-content">  **<h1>**Chapter 1</h1><div>Rails is sunshine and lollipops</div>**<h1>**Chapter 2</h1><div>Not everyone like Brussels Sprouts&nbsp;</div></div><!-- END vendor/bundle/ruby/3.3.0/gems/actiontext-8.0.0/app/views/action_text/contents/_content.html.erb --></div><!-- END app/views/layouts/action_text/contents/_content.html.erb -->

Another on a Slack channel suggested using Nokogiri. I installed the gem and rewrote the post.rb actions using it. I am still getting the same result. A helpful sort has suggested that the post.rb model may not be the best place for this. The best place I could think of to intercept and modify before save was the post model. I will think on this. Any advice would be appreciated. Here is the rewrite with Nokogiri:

class Post < ApplicationRecord  # ActionText body  has_rich_text :body  # ActionText toc  has_rich_text :toc  # Use separate callbacks for create and update  before_save :process_body  private  def process_body    # Extract the HTML content from the rich text body    body_content = body.to_trix_html    # Make modifications and create toc    result = generate_toc(body_content)    self.toc = result[:toc]  # Update the TOC    # Log result before save    Rails.logger.debug("MODIFIED BODY BEFORE SAVE: #{result[:content]}")    self.body = result[:content]  # Update the body with modified content    # Log result after save    Rails.logger.debug("BODY AFTER SAVE: #{self.body}")  end  def generate_toc(body)    # Parse the HTML body with Nokogiri    doc = Nokogiri::HTML::DocumentFragment.parse(body)    # Find all <h1> tags    headings = doc.css("h1")    return { toc: "", content: body } if headings.empty?    toc = "<ul>"    headings.each do |heading|      # Generate an ID for the heading      id = heading.text.gsub(/\s+/, "-").downcase      toc += "<li><a href='##{id}'>#{heading.text}</a></li>"      # Set the ID attribute on the heading      heading["id"] = id    end    toc += "</ul>"    # Return the TOC and the modified content    { toc: toc.html_safe, content: doc.to_html.html_safe }  endend

I would love any help here. At first I thought it was a issue with the loop and gsub, but now I am not so sure.


Viewing all articles
Browse latest Browse all 4634

Latest Images

Trending Articles



Latest Images