Class: Markdowndocs::Documentation

Inherits:
Object
  • Object
show all
Defined in:
app/models/markdowndocs/documentation.rb

Overview

Documentation PORO (Plain Old Ruby Object) Represents markdown documentation files from a configurable directory. Handles metadata extraction, frontmatter parsing, and category associations.

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(file_path) ⇒ Documentation

Returns a new instance of Documentation.



10
11
12
13
14
15
16
# File 'app/models/markdowndocs/documentation.rb', line 10

def initialize(file_path)
  @file_path = file_path
  @slug = derive_slug
  @path_slug = derive_path_slug
  
  @category = assign_category
end

Instance Attribute Details

#categoryObject (readonly)

Returns the value of attribute category.



8
9
10
# File 'app/models/markdowndocs/documentation.rb', line 8

def category
  @category
end

#descriptionObject (readonly)

Returns the value of attribute description.



8
9
10
# File 'app/models/markdowndocs/documentation.rb', line 8

def description
  @description
end

#file_pathObject (readonly)

Returns the value of attribute file_path.



8
9
10
# File 'app/models/markdowndocs/documentation.rb', line 8

def file_path
  @file_path
end

#keywordsObject (readonly)

Returns the value of attribute keywords.



8
9
10
# File 'app/models/markdowndocs/documentation.rb', line 8

def keywords
  @keywords
end

#path_slugObject (readonly)

Returns the value of attribute path_slug.



8
9
10
# File 'app/models/markdowndocs/documentation.rb', line 8

def path_slug
  @path_slug
end

#slugObject (readonly)

Returns the value of attribute slug.



8
9
10
# File 'app/models/markdowndocs/documentation.rb', line 8

def slug
  @slug
end

#titleObject (readonly)

Returns the value of attribute title.



8
9
10
# File 'app/models/markdowndocs/documentation.rb', line 8

def title
  @title
end

Class Method Details

.allObject



18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# File 'app/models/markdowndocs/documentation.rb', line 18

def self.all
  docs_path = Markdowndocs.config.resolved_docs_path
  return [] unless docs_path.exist?

  files = Dir.glob(docs_path.join("*.md"))

  modes = Markdowndocs.config.modes
  modes.each do |mode|
    mode_dir = docs_path.join(mode)
    files.concat(Dir.glob(mode_dir.join("*.md"))) if mode_dir.exist?
  end

  warn_about_non_mode_subdirectories(docs_path, modes)

  files.map { |f| new(Pathname.new(f)) }.sort_by(&:path_slug)
end

.by_category(category) ⇒ Object



105
106
107
# File 'app/models/markdowndocs/documentation.rb', line 105

def self.by_category(category)
  all.select { |doc| doc.category == category }
end

.find_by_slug(slug, mode: nil) ⇒ Object

Resolves a doc by slug. When ‘mode:` is given, prefers the mode-scoped file (docs/<mode>/<slug>.md) and falls back to the root (docs/<slug>.md) if visible_to?(mode) passes. With `mode: nil`, only the root is checked.



69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
# File 'app/models/markdowndocs/documentation.rb', line 69

def self.find_by_slug(slug, mode: nil)
  return nil if slug.blank?
  return nil if slug.include?("..") || slug.include?("/")

  docs_path = Markdowndocs.config.resolved_docs_path
  docs_root_real = docs_path.realpath

  if mode.present? && Markdowndocs.config.modes.include?(mode.to_s)
    scoped = docs_path.join(mode.to_s, "#{slug}.md")
    return new(scoped) if scoped.exist? && inside_docs_path?(scoped, docs_root_real)
  end

  root = docs_path.join("#{slug}.md")
  return nil unless root.exist? && inside_docs_path?(root, docs_root_real)

  doc = new(root)
  return nil unless doc.visible_to?(mode)

  doc
rescue => e
  Rails.logger.error("Error finding documentation by slug '#{slug}': #{e.message}")
  nil
end

.grouped_by_category(mode: nil) ⇒ Object

When ‘mode:` is given, filters out docs whose `audience:` excludes that mode AND drops categories that end up empty (so the index sidebar doesn’t render headers with no children).

Resolves slugs by matching against ‘path_slug` on the full discovered set so that path-prefixed slugs like “technical/architecture” (which `find_by_slug` rejects as directory traversal) are found correctly.



116
117
118
119
120
121
122
123
124
125
# File 'app/models/markdowndocs/documentation.rb', line 116

def self.grouped_by_category(mode: nil)
  all_docs = all
  Markdowndocs.config.categories.each_with_object({}) do |(category, slugs), hash|
    docs = slugs.filter_map do |slug|
      doc = all_docs.find { |d| d.path_slug == slug }
      doc if doc&.visible_to?(mode)
    end
    hash[category] = docs unless docs.empty?
  end
end

Instance Method Details

#audienceObject

The audience(s) this doc is written for. Resolution order:

1. `audience:` frontmatter (DEPRECATED in 0.7.0, removed in 1.0.0)
2. Parent directory name when it matches a configured mode
3. All configured modes (root file with no override — visible everywhere)


168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
# File 'app/models/markdowndocs/documentation.rb', line 168

def audience
  @audience ||= begin
    parsed = parse_frontmatter
    raw = parsed[:frontmatter]["audience"]

    if raw
      emit_audience_deprecation_warning_once
    end

    case raw
    when Array then raw.map(&:to_s)
    when String then [raw]
    when nil
      scope = audience_from_path
      scope ? [scope] : Markdowndocs.config.modes.dup
    else Markdowndocs.config.modes.dup
    end
  end
end

#available_modesObject



144
145
146
147
148
149
150
# File 'app/models/markdowndocs/documentation.rb', line 144

def available_modes
  @available_modes ||= begin
    parsed = parse_frontmatter
    modes = parsed[:frontmatter]["modes"]
    modes.is_a?(Array) ? modes.map(&:to_s) : Markdowndocs.config.modes.dup
  end
end

#cache_keyObject



134
135
136
# File 'app/models/markdowndocs/documentation.rb', line 134

def cache_key
  "#{path_slug.tr("/", "-")}-#{mtime.to_i}"
end

#code_contentObject

Returns text extracted from fenced code blocks for search indexing.



212
213
214
215
216
# File 'app/models/markdowndocs/documentation.rb', line 212

def code_content
  parsed = parse_frontmatter
  blocks = parsed[:markdown].scan(/```\w*\n([\s\S]*?)```/)
  blocks.flatten.join(" ").gsub(/\s+/, " ").strip
end

#contentObject



127
128
129
130
131
132
# File 'app/models/markdowndocs/documentation.rb', line 127

def content
  @content ||= file_path.read
rescue => e
  Rails.logger.error("Error reading documentation file '#{file_path}': #{e.message}")
  ""
end

#default_modeObject



152
153
154
155
156
157
158
# File 'app/models/markdowndocs/documentation.rb', line 152

def default_mode
  @default_mode ||= begin
    parsed = parse_frontmatter
    mode = parsed[:frontmatter]["default_mode"]
    mode.present? ? mode.to_s : Markdowndocs.config.default_mode
  end
end

#mtimeObject



138
139
140
141
142
# File 'app/models/markdowndocs/documentation.rb', line 138

def mtime
  @mtime ||= file_path.mtime
rescue
  Time.current
end

#plain_text_contentObject

Returns content stripped of frontmatter, markdown syntax, and HTML tags for use in search indexing.



198
199
200
201
202
203
204
205
206
207
208
209
# File 'app/models/markdowndocs/documentation.rb', line 198

def plain_text_content
  parsed = parse_frontmatter
  text = parsed[:markdown]
  text = text.gsub(/^#+\s*/, "")          # headings
  text = text.gsub(/\[([^\]]+)\]\([^)]+\)/, '\1') # links
  text = text.gsub(/[*_~`]/, "")          # emphasis markers
  text = text.gsub(/```[\s\S]*?```/, "")  # fenced code blocks
  text = text.gsub(/<[^>]+>/, "")         # HTML tags
  text = text.gsub(/^\s*[-*+]\s/, "")     # list markers
  text = text.gsub(/\n{2,}/, "\n")        # collapse blank lines
  text.strip
end

#supports_mode?(mode) ⇒ Boolean

Returns:

  • (Boolean)


160
161
162
# File 'app/models/markdowndocs/documentation.rb', line 160

def supports_mode?(mode)
  available_modes.include?(mode.to_s)
end

#visible_to?(mode) ⇒ Boolean

Whether this doc should be surfaced to a viewer in the given mode. ‘nil` mode is treated as “no filter” — useful for callers that don’t care about audience (search indexer, admin tools).

Returns:

  • (Boolean)


191
192
193
194
# File 'app/models/markdowndocs/documentation.rb', line 191

def visible_to?(mode)
  return true if mode.nil?
  audience.include?(mode.to_s)
end