Class: Markdowndocs::Documentation
- Inherits:
-
Object
- Object
- Markdowndocs::Documentation
- 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
-
#category ⇒ Object
readonly
Returns the value of attribute category.
-
#description ⇒ Object
readonly
Returns the value of attribute description.
-
#file_path ⇒ Object
readonly
Returns the value of attribute file_path.
-
#keywords ⇒ Object
readonly
Returns the value of attribute keywords.
-
#path_slug ⇒ Object
readonly
Returns the value of attribute path_slug.
-
#slug ⇒ Object
readonly
Returns the value of attribute slug.
-
#title ⇒ Object
readonly
Returns the value of attribute title.
Class Method Summary collapse
- .all ⇒ Object
- .by_category(category) ⇒ Object
-
.find_by_slug(slug, mode: nil) ⇒ Object
Resolves a doc by slug.
-
.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).
Instance Method Summary collapse
-
#audience ⇒ Object
The audience(s) this doc is written for.
- #available_modes ⇒ Object
- #cache_key ⇒ Object
-
#code_content ⇒ Object
Returns text extracted from fenced code blocks for search indexing.
- #content ⇒ Object
- #default_mode ⇒ Object
-
#initialize(file_path) ⇒ Documentation
constructor
A new instance of Documentation.
- #mtime ⇒ Object
-
#plain_text_content ⇒ Object
Returns content stripped of frontmatter, markdown syntax, and HTML tags for use in search indexing.
- #supports_mode?(mode) ⇒ Boolean
-
#visible_to?(mode) ⇒ Boolean
Whether this doc should be surfaced to a viewer in the given mode.
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
#category ⇒ Object (readonly)
Returns the value of attribute category.
8 9 10 |
# File 'app/models/markdowndocs/documentation.rb', line 8 def category @category end |
#description ⇒ Object (readonly)
Returns the value of attribute description.
8 9 10 |
# File 'app/models/markdowndocs/documentation.rb', line 8 def description @description end |
#file_path ⇒ Object (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 |
#keywords ⇒ Object (readonly)
Returns the value of attribute keywords.
8 9 10 |
# File 'app/models/markdowndocs/documentation.rb', line 8 def keywords @keywords end |
#path_slug ⇒ Object (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 |
#slug ⇒ Object (readonly)
Returns the value of attribute slug.
8 9 10 |
# File 'app/models/markdowndocs/documentation.rb', line 8 def slug @slug end |
#title ⇒ Object (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
.all ⇒ Object
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.}") 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
#audience ⇒ Object
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_modes ⇒ Object
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_key ⇒ Object
134 135 136 |
# File 'app/models/markdowndocs/documentation.rb', line 134 def cache_key "#{path_slug.tr("/", "-")}-#{mtime.to_i}" end |
#code_content ⇒ Object
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 |
#content ⇒ Object
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.}") "" end |
#default_mode ⇒ Object
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 |
#mtime ⇒ Object
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_content ⇒ Object
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
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).
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 |