Module: Legate::Web::DocumentationRoutes

Defined in:
lib/legate/web/routes/documentation_routes.rb

Defined Under Namespace

Modules: Helpers

Class Method Summary collapse

Class Method Details

.registered(app) ⇒ Object

module Helpers



88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
# File 'lib/legate/web/routes/documentation_routes.rb', line 88

def self.registered(app)
  app.helpers Helpers # Register the helpers for use in routes

  # Route for Documentation Index (GET /docs)
  app.get '/docs' do
    logger.debug('--- GET /docs --- (DocumentationRoutes)')
    docs_root_path = Pathname.new(File.join(settings.root, 'public', 'docs'))
    logger.debug("Docs root resolved to: #{docs_root_path}")

    categorized_documents = {}

    begin
      # Process files directly in docs_root_path first (General category)
      general_docs = []
      Dir.glob(File.join(docs_root_path, '*.md')).sort.each do |md_file_path|
        pathname = Pathname.new(md_file_path)
        next if pathname.directory? # Skip directories if any found by glob

        filename_md = pathname.basename.to_s
        filename_no_ext = pathname.basename('.md').to_s
        title = filename_no_ext.gsub(/[-_]/, ' ').split.map(&:capitalize).join(' ')
        file_content = File.read(md_file_path, encoding: 'UTF-8')
        first_h1_match = file_content.match(/^#\s+(.+)$/)
        title = first_h1_match[1].strip if first_h1_match
        summary = generate_summary(file_content)

        general_docs << {
          title: title,
          path: filename_no_ext, # Path relative to docs_root for linking
          summary: summary
        }
      end
      categorized_documents['General'] = general_docs if general_docs.any?

      # Process subdirectories for categories
      Dir.glob(File.join(docs_root_path, '*/')).sort.each do |dir_path|
        category_pathname = Pathname.new(dir_path)
        category_name = category_pathname.basename.to_s.gsub(/[-_]/, ' ').split.map(&:capitalize).join(' ')
        category_docs = []

        Dir.glob(File.join(category_pathname, '*.md')).sort.each do |md_file_path|
          pathname = Pathname.new(md_file_path)
          filename_md = pathname.basename.to_s
          filename_no_ext = pathname.basename('.md').to_s
          full_relative_path = "#{category_pathname.basename}/#{filename_no_ext}"

          title = filename_no_ext.gsub(/[-_]/, ' ').split.map(&:capitalize).join(' ')
          file_content = File.read(md_file_path, encoding: 'UTF-8')
          first_h1_match = file_content.match(/^#\s+(.+)$/)
          title = first_h1_match[1].strip if first_h1_match
          summary = generate_summary(file_content)

          category_docs << {
            title: title,
            path: full_relative_path, # Path includes category dir
            summary: summary
          }
        end
        categorized_documents[category_name] = category_docs if category_docs.any?
      end

      logger.warn("No markdown files or categories found in #{docs_root_path}") if categorized_documents.empty?
    rescue StandardError => e
      logger.error("Error scanning documentation directory: #{e.message}")
      logger.error(e.backtrace.first(5).join("\n"))
    end

    instance_variable_set(:@categorized_documents, categorized_documents)
    logger.debug("Setting @categorized_documents with #{categorized_documents.keys.count} categories.")
    logger.debug("Content of @categorized_documents: #{@categorized_documents.inspect}")

    slim :docs_index
  end

  # Route for Displaying a Single Document (GET /docs/*)
  # The splat parameter will capture the category/filename path
  app.get '/docs/*' do |path_splat|
    logger.debug("GET /docs/#{path_splat} - Entered (from DocumentationRoutes)")

    # Sanitize path_splat: allow alphanumeric, underscore, hyphen, and forward slash for path
    # Remove leading/trailing slashes and protect against directory traversal
    sane_path = path_splat.gsub(%r{^/+|/+$}, '').gsub(%r{\.{2}/}, '')
    sane_path.gsub!(%r{[^0-9a-zA-Z_\-/]}, '') # Allow alphanumeric, _, -, /

    if sane_path.empty?
      logger.warn("Attempt to access doc with invalid path: '#{path_splat}'")
      halt 404, slim(:error_404, locals: { title: 'Document Not Found', message: 'Invalid document path.' })
    end

    # Construct the full file path
    # Ensure it's .md, even if not explicitly in splat, for direct access attempts
    file_path_to_check = sane_path.end_with?('.md') ? sane_path : "#{sane_path}.md"
    full_file_path = File.join(settings.root, 'public', 'docs', file_path_to_check)
    logger.debug("Attempting to access document at: #{full_file_path}")

    # The render_markdown helper already performs security checks to ensure the file is within 'public/docs'
    # and is a .md file.

    begin
      markdown_html = render_markdown(full_file_path) # render_markdown expects absolute path

      if markdown_html.nil?
        logger.warn("Documentation file not found or rendering failed: #{full_file_path}")
        halt 404,
             slim(:error_404,
                  locals: { title: 'Document Not Found',
                            message: "Document '#{sane_path}' not found or could not be rendered." })
      end

      instance_variable_set(:@doc_html_content, markdown_html)

      # Try to extract title from H1 in markdown content
      # This requires reading the file again, or passing content from render_markdown if it returned it
      # For now, let's re-read for title extraction consistency.
      doc_title_for_view = sane_path.split('/').last.gsub(/[-_]/, ' ').split.map(&:capitalize).join(' ') # Default title
      if File.exist?(full_file_path)
        markdown_content_for_title = File.read(full_file_path, encoding: 'UTF-8')
        first_h1_match = markdown_content_for_title.match(/^#\s+(.+)$/)
        doc_title_for_view = first_h1_match[1].strip if first_h1_match
      end
      instance_variable_set(:@doc_title, doc_title_for_view)

      slim :docs_show
    rescue StandardError => e
      logger.error("Error processing document #{sane_path}: #{e.message}")
      logger.error(e.backtrace.join("\n"))
      halt 500, 'Error displaying document.'
    end
  end
end