auto-platform-guide 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 5ccccab1be99709c35e9851337b7f95f722124a4f23368de67c76c7e6539f6c5
4
+ data.tar.gz: cebdb9082d0502cd57550a0c567809bfee9beeb606faf7800b898dfa049da294
5
+ SHA512:
6
+ metadata.gz: 797fdaf7358ada5c81bf394f57f38490e2734c1bb9d269676d3d0c51e9b0513f7f9d1db9386a409c453f41f80f2951ddae076936e092967b58fd8db702e350fd
7
+ data.tar.gz: 6f6b428ebf18973783dc42b5ca427a41a8ab0b91bdffd9fad9ed972f8e617e13b7d784f5c499da9d2eacdc6dcdc8aaee0d380d672570aad39d0ec74a95c6e899
data/CHANGELOG.md ADDED
@@ -0,0 +1,44 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to
7
+ [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
8
+
9
+ ## [Unreleased]
10
+
11
+ ## [0.1.0] - 2026-10-26
12
+
13
+ Initial release: spec-driven doc generator, `doc_screenshot` helper, configurable
14
+ browser driver wiring, and markdown docs viewer.
15
+
16
+ ### Added
17
+
18
+ - Rake tasks for generating, checking, re-rendering and rebuilding the TOC,
19
+ shipped with the gem.
20
+ - `readme_preamble` configuration for the markdown above the generated README
21
+ table of contents.
22
+ - The gem's own guide in `docs/platform-guide/`, generated from `spec/docs/` and
23
+ verified in CI.
24
+
25
+ ### Changed
26
+
27
+ - Viewer links, breadcrumbs and wikilinks follow the `docs_path` configuration
28
+ instead of hardcoding `/docs`.
29
+ - The docs viewer template no longer depends on host helpers, partials or
30
+ assets (`content_header`, `shared/_box`, a host `docs_path` helper, `docs.js`).
31
+
32
+ ### Fixed
33
+
34
+ - Doc comments preserve indentation beyond the `# ` prefix, so fenced code
35
+ blocks keep their layout.
36
+ - Adjacent callout blockquotes render as separate callouts instead of one merged
37
+ quote.
38
+
39
+ ### Removed
40
+
41
+ - `bin/generate-docs`; use `auto_platform_guide:generate` instead.
42
+
43
+ [Unreleased]: https://github.com/mauriciozaffari/auto-platform-guide/compare/v0.1.0...HEAD
44
+ [0.1.0]: https://github.com/mauriciozaffari/auto-platform-guide/releases/tag/v0.1.0
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mauricio Zaffari
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,161 @@
1
+ # Auto Platform Guide
2
+
3
+ Generate in-app documentation from system specs.
4
+
5
+ The gem documents itself: `docs/platform-guide/` is generated from
6
+ `spec/docs/` by `rake auto_platform_guide:render` and checked in CI.
7
+
8
+ - `AutoPlatformGuide::Generator` renders markdown guides from `# =` doc comments in
9
+ system specs into `generated_docs_dir`, with an ordered README TOC grouped by
10
+ `@category`/`@order` directives.
11
+ - `AutoPlatformGuide.setup_rspec(config)` wires system-spec screenshot support:
12
+ `doc_screenshot('name')` captures to `screenshots_dir` when `SCREENSHOTS=true`,
13
+ and specs run on `rack_test` unless they need a browser (`:js` tag or
14
+ `SCREENSHOTS=true`). Browser driver is `:playwright` by default; set
15
+ `config.browser_driver = :selenium` to use Selenium.
16
+ - The markdown viewer: `Document` + the `docs/show` template, rendered through
17
+ `AutoPlatformGuide::DocsControllerActions`. Include it in the host app's docs
18
+ controller so authentication stays application-owned:
19
+
20
+ ```ruby
21
+ class DocsController < AdminController
22
+ include AutoPlatformGuide::DocsControllerActions
23
+ end
24
+ ```
25
+
26
+ ```ruby
27
+ # config/routes.rb
28
+ get '/docs(/*id)', to: 'docs#show', as: 'docs'
29
+ ```
30
+
31
+ The shipped `docs/show` template uses plain markup and inline JavaScript; it
32
+ needs no host helpers, partials or assets. Links are built from `docs_path`, so
33
+ set it to the path you mount (default `/docs`). Mermaid fenced blocks render
34
+ when a global `mermaid` is present and stay readable as source otherwise.
35
+ Override the template in the host app to change the surrounding chrome.
36
+
37
+ ## Installation
38
+
39
+ ```ruby
40
+ # Gemfile
41
+ gem 'auto-platform-guide'
42
+ ```
43
+
44
+ Tagged releases publish to RubyGems automatically.
45
+
46
+ ## Setup
47
+
48
+ ```ruby
49
+ # config/initializers/auto_platform_guide.rb
50
+ AutoPlatformGuide.configure do |config|
51
+ config.docs_root = 'docs'
52
+ config.docs_path = '/docs'
53
+ config.generated_docs_dir = 'docs/platform-guide'
54
+ config.screenshots_dir = 'docs/screenshots'
55
+ config.specs_glob = 'spec/system/**/*_spec.rb'
56
+ config.browser_driver = :playwright # or :selenium
57
+ config.readme_preamble = nil
58
+ end
59
+ ```
60
+
61
+ Every key is optional; the values above are the defaults. Keep
62
+ `generated_docs_dir` inside `docs_root`, since the viewer only reads from
63
+ `docs_root`.
64
+
65
+ The `:playwright` default needs `capybara-playwright-driver` in the host app;
66
+ `:selenium` needs `selenium-webdriver`. `capybara` itself is a gem dependency.
67
+ `readme_preamble` is markdown placed above the generated README's table of
68
+ contents; `nil` keeps `AutoPlatformGuide::Generator::README_PREAMBLE`. Note:
69
+ `:selenium` support is unit-tested with mocked drivers — no CI job boots a real
70
+ browser under either driver.
71
+
72
+ ```ruby
73
+ # spec/rails_helper.rb
74
+ require 'auto_platform_guide/rspec'
75
+
76
+ RSpec.configure do |config|
77
+ AutoPlatformGuide.setup_rspec(config)
78
+ end
79
+ ```
80
+
81
+ ## Writing a documented spec
82
+
83
+ Doc comments are parsed from the files matched by `specs_glob`. A spec file is
84
+ picked up when it has a `# = Heading` at column 0.
85
+
86
+ ```ruby
87
+ # spec/system/orders_spec.rb
88
+ require 'rails_helper'
89
+
90
+ # @category Orders
91
+ # @order 200
92
+ #
93
+ # = Orders
94
+ # Create and track customer orders.
95
+ #
96
+ # == Creating an order
97
+ # Pick a customer, add line items and save.
98
+
99
+ RSpec.describe 'Orders', type: :system do
100
+ it 'creates an order' do
101
+ visit new_admin_order_path
102
+
103
+ fill_in 'Reference', with: 'ORD-1'
104
+ click_button 'Save'
105
+ expect(page).to have_content('Order created')
106
+
107
+ doc_screenshot('orders/created')
108
+ end
109
+ end
110
+ ```
111
+
112
+ - `# = Heading` renders as `<h1>`; every extra `=` adds a level (`# ==` → `<h2>`).
113
+ - Comment lines after a heading are its body, up to the next heading or the first
114
+ non-comment line. Sections can also sit directly above the examples they describe.
115
+ - `@category` and `@order` only place the guide in the README TOC; they are
116
+ stripped from the body. Specs without them fall under `Other` / `999_999`.
117
+ - The generator parses comments and embeds only screenshots whose PNG already
118
+ exists on disk. `generate` runs the spec first to capture them; `render` and
119
+ `check` never run specs.
120
+
121
+ `doc_screenshot('orders/created')` writes `docs/screenshots/orders/created.png`
122
+ when screenshots are enabled, and does nothing otherwise.
123
+
124
+ ## Capturing screenshots
125
+
126
+ ```bash
127
+ SCREENSHOTS=true bundle exec rspec spec/system/orders_spec.rb
128
+ rake auto_platform_guide:generate
129
+ ```
130
+
131
+ `SCREENSHOTS=true` drives every system example in that run through the configured
132
+ browser (`:playwright` by default) and saves each `doc_screenshot` call. Without
133
+ it, system specs use `rack_test`; tag individual examples `:js` when only they
134
+ need a browser.
135
+
136
+ The helper is included for `type: :system` only, so keep that metadata. A
137
+ screenshot is rendered after the heading block that precedes its call.
138
+
139
+ ## Rake tasks
140
+
141
+ Generating docs is a rake task shipped with the gem. Rails apps load it through
142
+ the engine; anywhere else, require it from your `Rakefile`:
143
+
144
+ ```ruby
145
+ require 'auto_platform_guide/rake_tasks'
146
+ ```
147
+
148
+ ```bash
149
+ rake auto_platform_guide:generate # all documented specs
150
+ rake "auto_platform_guide:generate[spec/foo_spec.rb]" # one spec
151
+ rake auto_platform_guide:check # verify docs; non-zero when stale
152
+ rake auto_platform_guide:readme # rebuild the README TOC
153
+ rake auto_platform_guide:render # re-render, reuse screenshots on disk
154
+ ```
155
+
156
+ ## Tests
157
+
158
+ ```bash
159
+ bin/setup
160
+ bundle exec rspec
161
+ ```
@@ -0,0 +1,46 @@
1
+ <% content_for :title, @document.title %>
2
+
3
+ <section class="content-header">
4
+ <h1><%= @document.title %></h1>
5
+
6
+ <ol class="breadcrumb">
7
+ <li><%= link_to 'Documentation', AutoPlatformGuide::Document.docs_path %></li>
8
+ <% @document.breadcrumbs.each do |crumb| %>
9
+ <li><%= link_to crumb[:name], crumb[:path] %></li>
10
+ <% end %>
11
+ </ol>
12
+ </section>
13
+
14
+ <section class="content documentation">
15
+ <div class="box">
16
+ <div class="box-body">
17
+ <%= @document.render %>
18
+ </div>
19
+ </div>
20
+ </section>
21
+
22
+ <%# Mermaid diagrams are an enhancement: without a global `mermaid`, the
23
+ renderer's <pre class="mermaid"> blocks stay visible as source. %>
24
+ <%= javascript_tag nonce: true do %>
25
+ (() => {
26
+ if (window.autoPlatformGuideDocsReady) return
27
+ window.autoPlatformGuideDocsReady = true
28
+
29
+ const renderDiagrams = () => {
30
+ if (!window.mermaid) return
31
+
32
+ const nodes = document.querySelectorAll('pre.mermaid:not([data-processed="true"])')
33
+ if (!nodes.length) return
34
+
35
+ window.mermaid.initialize({
36
+ startOnLoad: false,
37
+ securityLevel: 'strict',
38
+ theme: window.darkMode ? 'dark' : 'default'
39
+ })
40
+ window.mermaid.run({ nodes })
41
+ }
42
+
43
+ document.addEventListener('turbo:load', renderDiagrams)
44
+ if (document.readyState === 'complete') renderDiagrams()
45
+ })()
46
+ <% end %>
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'auto_platform_guide'
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AutoPlatformGuide
4
+ class Configuration
5
+ # Where the viewer looks for markdown. The generator writes under
6
+ # generated_docs_dir, which lives inside this directory.
7
+ attr_accessor :docs_root
8
+ # Where DocGenerator writes the generated guides (specs -> markdown).
9
+ attr_accessor :generated_docs_dir
10
+ # Where doc_screenshot writes PNGs.
11
+ attr_accessor :screenshots_dir
12
+ # Glob (relative to repo root) for specs searched for doc comments.
13
+ attr_accessor :specs_glob
14
+ # Browser driver used when a system spec needs a real browser:
15
+ # :playwright (default) or :selenium.
16
+ attr_accessor :browser_driver
17
+ # Markdown written above the generated README table of contents. Defaults to
18
+ # Generator::README_PREAMBLE when nil.
19
+ attr_accessor :readme_preamble
20
+ # URL prefix where the docs viewer is mounted. Links, breadcrumbs and
21
+ # wikilinks are built from it, so it must match the host's route.
22
+ attr_reader :docs_path
23
+
24
+ def initialize
25
+ @docs_root = 'docs'
26
+ @generated_docs_dir = 'docs/platform-guide'
27
+ @screenshots_dir = 'docs/screenshots'
28
+ @specs_glob = 'spec/system/**/*_spec.rb'
29
+ @browser_driver = :playwright
30
+ @readme_preamble = nil
31
+ @docs_path = '/docs'
32
+ end
33
+
34
+ def docs_path=(path)
35
+ @docs_path = path.chomp('/')
36
+ end
37
+ end
38
+
39
+ class << self
40
+ def configuration
41
+ @configuration ||= Configuration.new
42
+ end
43
+
44
+ def configure
45
+ yield configuration
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AutoPlatformGuide
4
+ # Include into the host app's docs controller. The host keeps its own
5
+ # controller class so authentication (before_actions, layouts) stays
6
+ # application-owned:
7
+ #
8
+ # class DocsController < AdminController
9
+ # include AutoPlatformGuide::DocsControllerActions
10
+ # end
11
+ module DocsControllerActions
12
+ def show
13
+ @document = Document.find(params[:id] || 'README')
14
+ # Markdown links end in `.md`, which Rails would otherwise read as the response format.
15
+ render template: 'docs/show', formats: :html
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,219 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'erb'
4
+ require 'pathname'
5
+ require 'redcarpet'
6
+ require 'rouge'
7
+ require 'active_model'
8
+ require 'action_view'
9
+ require 'active_support/core_ext/enumerable'
10
+ require 'active_support/core_ext/module/delegation'
11
+ require 'active_support/core_ext/string/inflections'
12
+
13
+ module AutoPlatformGuide
14
+ # A markdown document under the configured docs root, rendered for /docs.
15
+ class Document
16
+ include ActiveModel::Model
17
+
18
+ class RecordNotFound < StandardError; end
19
+
20
+ FOLDER_INDEX_NAMES = ['README', '00 - Index'].freeze
21
+
22
+ class Renderer < Redcarpet::Render::HTML
23
+ include ActionView::Helpers::TagHelper
24
+ include ActionView::Helpers::SanitizeHelper
25
+
26
+ # Obsidian wikilink: [[Note]], [[Note#Heading]], [[Note|Alias]] (and combinations).
27
+ WIKILINK = /\[\[([^\]#|]+)(?:#([^\]|]+))?(?:\|([^\]]+))?\]\]/
28
+ CALLOUT = /\A<p>\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\](?:<br>\s*)?/i
29
+ CALLOUT_ICONS = {
30
+ 'note' => 'info-circle',
31
+ 'tip' => 'lightbulb-o',
32
+ 'important' => 'exclamation-circle',
33
+ 'warning' => 'warning',
34
+ 'caution' => 'ban'
35
+ }.freeze
36
+
37
+ # Redcarpet merges adjacent blockquotes into one callback, so split the
38
+ # quote at each callout marker and render them separately.
39
+ CALLOUT_SPLIT = /(?=<p>\[!(?:NOTE|TIP|IMPORTANT|WARNING|CAUTION)\])/i
40
+
41
+ def self.render(path, base_dir: nil)
42
+ new(base_dir:).render(path)
43
+ end
44
+
45
+ def initialize(base_dir: nil, docs_path: nil, **options)
46
+ @base_dir = base_dir
47
+ @docs_path = (docs_path || Document.docs_path).chomp('/')
48
+ super(options.merge(with_toc_data: true, hard_wrap: true))
49
+ end
50
+
51
+ def sanitize(html)
52
+ tags = ActionView::Base.sanitized_allowed_tags + %w[table thead tbody th tr td img i]
53
+ attributes = ActionView::Base.sanitized_allowed_attributes + %w[style colspan src alt id class aria-hidden]
54
+ super(html, tags:, attributes:)
55
+ end
56
+
57
+ def markdown
58
+ @markdown ||= Redcarpet::Markdown.new(
59
+ self,
60
+ fenced_code_blocks: true,
61
+ tables: true,
62
+ autolink: true,
63
+ strikethrough: true
64
+ )
65
+ end
66
+
67
+ def render(path)
68
+ sanitize(markdown.render(File.read(path)))
69
+ end
70
+
71
+ # Convert Obsidian wikilinks to markdown links before parsing, leaving any
72
+ # text inside fenced or inline code untouched.
73
+ def preprocess(document)
74
+ replace_wikilinks(document)
75
+ end
76
+
77
+ def block_code(code, language)
78
+ # Mermaid diagrams are rendered client-side: emit the raw source in a
79
+ # <pre class="mermaid"> for mermaid.js, never syntax-highlighted.
80
+ return content_tag(:pre, code, class: 'mermaid') if language == 'mermaid'
81
+
82
+ lexer = Rouge::Lexer.find_fancy(language.to_s, code) || Rouge::Lexers::PlainText.new
83
+ formatter = Rouge::Formatters::HTMLInline.new(Rouge::Themes::Github.mode(:dark).new)
84
+ code = formatter.format(lexer.lex(code))
85
+ content_tag(:pre, class: 'code-block') do
86
+ content_tag(:code, sanitize(code), class: "language-#{lexer.tag}")
87
+ end
88
+ end
89
+
90
+ def image(link, title, alt_text)
91
+ # tag.img renders HTML5 without the self-closing slash the specs assert.
92
+ tag(:img, src: link, alt: alt_text, title:, class: 'img-responsive') # rubocop:disable Rails/ContentTag
93
+ end
94
+
95
+ def block_quote(quote)
96
+ quote.split(CALLOUT_SPLIT).reject(&:empty?).map { |segment| render_quote(segment) }.join("\n")
97
+ end
98
+
99
+ private
100
+
101
+ def render_quote(segment)
102
+ callout = segment.match(CALLOUT)
103
+ return %(<blockquote>#{segment}</blockquote>) unless callout
104
+
105
+ type = callout[1].downcase
106
+ body = segment.sub(CALLOUT, '<p>')
107
+ icon = tag.i(class: "fa fa-#{CALLOUT_ICONS.fetch(type)}", aria: { hidden: true })
108
+ title = %(<p class="callout-title">#{icon} #{type.titleize}</p>)
109
+
110
+ %(<blockquote class="callout callout-#{type}">#{title}\n#{body}</blockquote>)
111
+ end
112
+
113
+ def replace_wikilinks(document)
114
+ in_fence = false
115
+
116
+ document.each_line.map do |line|
117
+ if line.start_with?('```')
118
+ in_fence = !in_fence
119
+ line
120
+ elsif in_fence
121
+ line
122
+ else
123
+ replace_wikilinks_in_line(line)
124
+ end
125
+ end.join
126
+ end
127
+
128
+ def replace_wikilinks_in_line(line)
129
+ line.split(/(`[^`]*`)/).map do |part|
130
+ if part.start_with?('`')
131
+ part
132
+ else
133
+ part.gsub(WIKILINK) do
134
+ wikilink_markdown(::Regexp.last_match(1), ::Regexp.last_match(2), ::Regexp.last_match(3))
135
+ end
136
+ end
137
+ end.join
138
+ end
139
+
140
+ def wikilink_markdown(name, heading, alias_text)
141
+ name = name.strip
142
+ label = alias_text&.strip
143
+ label ||= heading ? "#{name} › #{heading.strip}" : name
144
+ "[#{escape_markdown(label)}](#{wikilink_url(name, heading)})"
145
+ end
146
+
147
+ # Escape characters the link label would otherwise trigger as markdown
148
+ # (emphasis, links, code) so snake_case names render literally.
149
+ def escape_markdown(text)
150
+ text.gsub(/[\\_*\[\]`]/) { |char| "\\#{char}" }
151
+ end
152
+
153
+ def wikilink_url(name, heading)
154
+ segments = [@base_dir, name].compact_blank
155
+ url = "#{@docs_path}/#{segments.map { |segment| ERB::Util.url_encode(segment) }.join('/')}"
156
+ url += "##{heading_anchor(heading)}" if heading
157
+ url
158
+ end
159
+
160
+ # Mirror Redcarpet's with_toc_data anchor generation for [[Note#Heading]]:
161
+ # drop non-alphanumerics (underscores included), then collapse whitespace to "-".
162
+ def heading_anchor(heading)
163
+ heading.strip.downcase.gsub(/[^a-z0-9\s]/, '').gsub(/\s+/, '-').gsub(/^-|-$/, '')
164
+ end
165
+ end
166
+
167
+ attr_reader :id, :name, :path
168
+
169
+ class << self
170
+ def where(folder: nil, name: nil)
171
+ prefix = folder ? "#{docs_root}/#{folder}/" : "{#{docs_root}/**/,}"
172
+ Rails.root.glob("#{prefix}#{name || '*'}.md").sort.map { |path| new(path) }
173
+ end
174
+
175
+ alias all where
176
+ delegate :first, :last, to: :where
177
+
178
+ def find(name)
179
+ # .lazy stops at the first index file that exists.
180
+ where(name:).first ||
181
+ FOLDER_INDEX_NAMES.lazy.filter_map { |index| where(folder: name, name: index).first }.first ||
182
+ raise(RecordNotFound, "Couldn't find document #{name}")
183
+ end
184
+
185
+ def docs_root
186
+ AutoPlatformGuide.configuration.docs_root
187
+ end
188
+
189
+ def docs_path
190
+ AutoPlatformGuide.configuration.docs_path
191
+ end
192
+ end
193
+
194
+ def initialize(path)
195
+ @path = path
196
+ relative_path = Pathname(path).relative_path_from(Rails.root).to_s
197
+ prefix = Regexp.escape(AutoPlatformGuide.configuration.docs_root)
198
+ @id = relative_path.match(%r{\A(?:#{prefix}/)?(?<id>.*)\.md\z})&.[](:id)
199
+ @name = File.basename(path, '.md')
200
+ end
201
+
202
+ def render
203
+ dir = File.dirname(id)
204
+ Renderer.render(path, base_dir: dir == '.' ? nil : dir)
205
+ end
206
+
207
+ def title
208
+ name.titleize
209
+ end
210
+
211
+ def breadcrumbs
212
+ parts = id.split('/')
213
+
214
+ parts.each_with_index.map do |part, i|
215
+ { name: part.titleize, path: "#{self.class.docs_path}/#{parts[0..i].join('/')}" }
216
+ end
217
+ end
218
+ end
219
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/engine'
4
+
5
+ module AutoPlatformGuide
6
+ class Engine < ::Rails::Engine
7
+ initializer 'auto_platform_guide.view_paths', before: :add_view_paths do |app|
8
+ app.config.paths['app/views'].push(root.join('app', 'views'))
9
+ end
10
+
11
+ rake_tasks do
12
+ require 'auto_platform_guide/rake_tasks'
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,300 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'pathname'
5
+
6
+ module AutoPlatformGuide
7
+ # Generates markdown guides from system specs with doc comments, plus the
8
+ # README table of contents. Doc comments are `# = Heading` blocks in spec
9
+ # files; `doc_screenshot('name')` calls reference PNGs captured during a
10
+ # SCREENSHOTS=true run.
11
+ class Generator
12
+ # Fallbacks for specs that omit the directives below. Specs with no
13
+ # `@category` collect under DEFAULT_CATEGORY; specs with no `@order` sort
14
+ # after numbered ones. Both defaults push such docs to the end of the TOC.
15
+ DEFAULT_CATEGORY = 'Other'
16
+ DEFAULT_ORDER = 999_999
17
+
18
+ FLAGS = %w[--check --readme-only --render-only].freeze
19
+
20
+ README_PREAMBLE = <<~MARKDOWN
21
+ <h1 id="platform-guide">Platform Guide</h1>
22
+
23
+ This guide is the primary, automatically generated reference for this
24
+ application. It is extracted from documented specs, so every page reflects
25
+ behaviour that is actually tested in the codebase.
26
+
27
+ Guides are grouped by area below and ordered to follow a typical workflow, from
28
+ the first area through to the last.
29
+ MARKDOWN
30
+
31
+ # Returns a process exit status (0 ok, 1 stale in --check mode).
32
+ # The bin wrapper passes it straight to exit.
33
+ def self.run(argv)
34
+ spec_files = argv - FLAGS
35
+ generator = new(spec_files.empty? ? nil : spec_files)
36
+
37
+ case FLAGS.find { |flag| argv.include?(flag) }
38
+ when '--check'
39
+ # Verify docs without running specs; used by CI.
40
+ generator.up_to_date? ? 0 : 1
41
+ when '--readme-only'
42
+ # Rebuild the TOC from committed docs + spec directives only (fast).
43
+ generator.regenerate_readme
44
+ 0
45
+ when '--render-only'
46
+ # Re-render doc bodies from doc comments using screenshots on disk.
47
+ generator.generate(run_specs: false)
48
+ 0
49
+ else
50
+ generator.generate
51
+ 0
52
+ end
53
+ end
54
+
55
+ def initialize(spec_files = nil)
56
+ @spec_files = spec_files || find_documented_specs
57
+ end
58
+
59
+ def config
60
+ AutoPlatformGuide.configuration
61
+ end
62
+
63
+ def generate(run_specs: true)
64
+ if @spec_files.empty?
65
+ puts 'No system specs with documentation comments found.'
66
+ return
67
+ end
68
+
69
+ @spec_files.each do |spec_file|
70
+ puts "\nGenerating documentation for #{spec_file}..."
71
+ process_spec(spec_file, run_specs:)
72
+ end
73
+
74
+ generate_readme
75
+
76
+ puts "\n✓ Documentation generation complete!"
77
+ puts " Generated #{@spec_files.size} documentation file(s) in #{config.generated_docs_dir}/"
78
+ puts ' Generated README.md with table of contents'
79
+ end
80
+
81
+ # Verify the committed docs are in sync with their documented specs,
82
+ # without running the specs. Re-renders the doc body and compares against
83
+ # the committed markdown, ignoring the generation timestamp and the
84
+ # screenshot binaries themselves. Returns true when everything is up to date.
85
+ def up_to_date?
86
+ stale = find_documented_specs.select { |spec_file| doc_stale?(spec_file) }
87
+
88
+ if stale.empty?
89
+ puts '✓ Platform-guide docs are up to date.'
90
+ return true
91
+ end
92
+
93
+ warn "✗ Platform-guide docs are out of date for #{stale.size} spec(s):"
94
+ stale.each { |spec_file| warn " - #{spec_file} -> #{generate_output_filename(spec_file)}" }
95
+ warn ''
96
+ warn 'Regenerate the affected guide(s) and commit the result:'
97
+ stale.each { |spec_file| warn %( rake "auto_platform_guide:generate[#{spec_file}]") }
98
+ false
99
+ end
100
+
101
+ # Rebuild only the README table of contents from the committed docs and
102
+ # their spec `@category`/`@order` directives, without running any specs.
103
+ def regenerate_readme
104
+ generate_readme
105
+ puts '✓ Regenerated README.md table of contents.'
106
+ end
107
+
108
+ private
109
+
110
+ def doc_stale?(spec_file)
111
+ output_file = generate_output_filename(spec_file)
112
+ return true unless File.exist?(output_file)
113
+
114
+ expected = "#{render_doc_body(spec(spec_file).doc_comments, spec(spec_file).screenshot_calls).rstrip}\n"
115
+ committed = strip_timestamp_header(File.read(output_file, encoding: 'UTF-8'))
116
+ expected != committed
117
+ end
118
+
119
+ def strip_timestamp_header(content)
120
+ content.sub(/\A<!--\nGenerated: [^\n]*\n-->\n\n/, '')
121
+ end
122
+
123
+ def find_documented_specs
124
+ Dir.glob(config.specs_glob).select { |file| spec(file).documented? }
125
+ end
126
+
127
+ # Parsed spec, memoized so a run reads each file once.
128
+ def spec(spec_file)
129
+ @specs ||= {}
130
+ @specs[spec_file] ||= SpecFile.new(spec_file)
131
+ end
132
+
133
+ def process_spec(spec_file, run_specs: true)
134
+ parsed = spec(spec_file)
135
+
136
+ if run_specs
137
+ puts ' Running spec with SCREENSHOTS=true...'
138
+ run_spec_with_screenshots(spec_file)
139
+ else
140
+ puts ' Re-rendering from doc comments (using existing screenshots)...'
141
+ end
142
+
143
+ # Generate markdown file
144
+ output_file = generate_output_filename(spec_file)
145
+ generate_markdown(output_file, parsed.doc_comments, parsed.screenshot_calls)
146
+
147
+ puts " ✓ Generated #{output_file}"
148
+ end
149
+
150
+ def run_spec_with_screenshots(spec_file)
151
+ FileUtils.mkdir_p(config.screenshots_dir)
152
+
153
+ command = "SCREENSHOTS=true bundle exec rspec --format documentation #{spec_file}"
154
+ system(command, err: File::NULL)
155
+ end
156
+
157
+ def generate_output_filename(spec_file)
158
+ basename = File.basename(spec_file, '_spec.rb')
159
+ output_name = "#{basename.upcase.tr('_', '-')}.md"
160
+ File.join(config.generated_docs_dir, output_name)
161
+ end
162
+
163
+ def generate_markdown(output_file, doc_comments, screenshot_calls)
164
+ FileUtils.mkdir_p(config.generated_docs_dir)
165
+ File.write(output_file, "#{timestamp_header}#{render_doc_body(doc_comments, screenshot_calls).rstrip}\n")
166
+ end
167
+
168
+ def timestamp_header
169
+ "<!--\nGenerated: #{generated_at}\n-->\n\n"
170
+ end
171
+
172
+ def generated_at
173
+ Time.now.utc.strftime('%Y-%m-%d %H:%M:%S UTC')
174
+ end
175
+
176
+ # Render the markdown body (everything after the timestamp header) from a
177
+ # spec's doc comments and screenshot calls. Shared by generation and --check
178
+ # so both produce byte-identical output for the same spec.
179
+ def render_doc_body(doc_comments, screenshot_calls)
180
+ body = +''
181
+
182
+ doc_comments.each_with_index do |comment, index|
183
+ heading_tag = "h#{comment[:level]}"
184
+ heading_id = slugify(comment[:title])
185
+ body << "<#{heading_tag} id=\"#{heading_id}\">#{comment[:title]}</#{heading_tag}>\n\n"
186
+
187
+ comment[:content].each { |line| body << "#{line}\n" }
188
+ body << "\n"
189
+
190
+ next_comment_line = doc_comments[index + 1]&.dig(:line_number) || Float::INFINITY
191
+ screenshots = find_related_screenshots(comment[:line_number], next_comment_line, screenshot_calls)
192
+ screenshots.each do |screenshot|
193
+ rendered = render_screenshot(screenshot[:path])
194
+ body << rendered if rendered
195
+ end
196
+ end
197
+
198
+ body
199
+ end
200
+
201
+ def find_related_screenshots(current_line, next_line, screenshot_calls)
202
+ # Find screenshots that appear after this comment but before the next one
203
+ screenshot_calls.select do |screenshot|
204
+ screenshot[:line_number] > current_line && screenshot[:line_number] < next_line
205
+ end
206
+ end
207
+
208
+ def render_screenshot(screenshot_path)
209
+ # Only embed screenshots whose generated file exists on disk. Image URLs
210
+ # mirror screenshots_dir so a customized dir keeps working links.
211
+ return unless File.exist?(File.join(config.screenshots_dir, "#{screenshot_path}.png"))
212
+
213
+ prefix = "/#{config.screenshots_dir.sub(%r{\A/+}, '').sub(%r{/+\z}, '')}"
214
+ "![#{screenshot_path}](#{prefix}/#{screenshot_path}.png)\n\n"
215
+ end
216
+
217
+ def slugify(text)
218
+ text.downcase
219
+ .gsub(/[^a-z0-9\s-]/, '')
220
+ .gsub(/\s+/, '-').squeeze('-')
221
+ .gsub(/^-|-$/, '')
222
+ end
223
+
224
+ # Writes the README table of contents grouped by `@category` and ordered by
225
+ # `@order`. Called at the end of a full/partial generation run and standalone
226
+ # via `regenerate_readme` (the `--readme-only` mode).
227
+ def generate_readme
228
+ readme_path = File.join(config.generated_docs_dir, 'README.md')
229
+
230
+ File.open(readme_path, 'w') do |f|
231
+ f.puts '<!--'
232
+ f.puts "Generated: #{generated_at}"
233
+ f.puts '-->'
234
+ f.puts
235
+
236
+ f.write(config.readme_preamble || README_PREAMBLE)
237
+
238
+ # Build the TOC from every documented spec (not just the ones regenerated
239
+ # in this run) so running for a subset of specs doesn't drop other
240
+ # entries. Group by `@category` and order by `@order`.
241
+ groups = toc_groups
242
+
243
+ f.puts category_workflow_diagram(groups.map(&:first))
244
+ f.puts
245
+ f.puts '<h2 id="table-of-contents">Table of Contents</h2>'
246
+
247
+ groups.each do |category, entries|
248
+ f.puts
249
+ f.puts %(<h3 id="#{slugify(category)}">#{category}</h3>)
250
+ f.puts
251
+ entries.each do |entry|
252
+ f.puts "- [#{entry[:title]}](#{entry[:file]})"
253
+ end
254
+ end
255
+ end
256
+ end
257
+
258
+ # A Mermaid flowchart of the guide categories in workflow order, rendered by
259
+ # the in-app docs viewer. Derived from the same ordered categories as the TOC,
260
+ # so it never drifts from the actual guides.
261
+ def category_workflow_diagram(categories)
262
+ return '' if categories.empty?
263
+
264
+ nodes = categories.each_with_index.map { |category, index| %(c#{index}["#{category}"]) }
265
+ <<~DIAGRAM.chomp
266
+
267
+ ```mermaid
268
+ flowchart LR
269
+ #{nodes.join(' --> ')}
270
+ ```
271
+ DIAGRAM
272
+ end
273
+
274
+ # Returns the TOC as an ordered list of [category, entries] pairs.
275
+ # Category order is derived entirely from the specs: categories are
276
+ # listed in ascending order of the smallest `@order` they contain (ties broken
277
+ # alphabetically), and entries within a category are ordered by `@order` then
278
+ # title. Only specs whose generated doc actually exists on disk are linked.
279
+ def toc_groups
280
+ entries = find_documented_specs.filter_map do |spec_file|
281
+ file = File.basename(generate_output_filename(spec_file))
282
+ next unless File.exist?(File.join(config.generated_docs_dir, file))
283
+
284
+ parsed = spec(spec_file)
285
+ directives = parsed.directives
286
+ {
287
+ file:,
288
+ title: parsed.title,
289
+ category: directives[:category] || DEFAULT_CATEGORY,
290
+ order: directives[:order] || DEFAULT_ORDER
291
+ }
292
+ end
293
+
294
+ entries
295
+ .group_by { |entry| entry[:category] }
296
+ .sort_by { |category, group| [group.map { |entry| entry[:order] }.min, category] }
297
+ .map { |category, group| [category, group.sort_by { |entry| [entry[:order], entry[:title]] }] }
298
+ end
299
+ end
300
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'auto_platform_guide'
4
+
5
+ module AutoPlatformGuide
6
+ # Rake task bodies. Tasks themselves are declared below; this module keeps the
7
+ # exit-status handling testable without invoking rake.
8
+ module RakeTasks
9
+ module_function
10
+
11
+ def run(argv, failure:)
12
+ status = Generator.run(argv)
13
+ return if status.zero?
14
+
15
+ abort(failure)
16
+ end
17
+ end
18
+ end
19
+
20
+ namespace :auto_platform_guide do
21
+ desc 'Generate guides from every documented system spec'
22
+ task :generate, [:spec_file] do |_task, args|
23
+ spec_files = [args[:spec_file], *args.extras].compact
24
+ AutoPlatformGuide::RakeTasks.run(spec_files, failure: 'Guide generation failed')
25
+ end
26
+
27
+ desc 'Verify committed guides match their specs; fails when stale'
28
+ task :check do
29
+ AutoPlatformGuide::RakeTasks.run(
30
+ ['--check'],
31
+ failure: 'Committed guides are stale; run rake auto_platform_guide:generate'
32
+ )
33
+ end
34
+
35
+ desc 'Rebuild only the README table of contents'
36
+ task :readme do
37
+ AutoPlatformGuide::RakeTasks.run(['--readme-only'], failure: 'README generation failed')
38
+ end
39
+
40
+ desc 'Re-render guides from doc comments, reusing screenshots on disk'
41
+ task :render do
42
+ AutoPlatformGuide::RakeTasks.run(['--render-only'], failure: 'Guide rendering failed')
43
+ end
44
+ end
@@ -0,0 +1,133 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/core_ext/object/blank'
4
+ require 'capybara'
5
+
6
+ module AutoPlatformGuide
7
+ # RSpec wiring for system specs: a rack_test default, a real browser where a
8
+ # spec needs one (examples tagged :js, plus every system spec when
9
+ # SCREENSHOTS=true), doc_screenshot support, and forgery protection.
10
+ #
11
+ # RSpec.configure do |config|
12
+ # AutoPlatformGuide.setup_rspec(config)
13
+ # end
14
+ #
15
+ # The browser driver is configuration.browser_driver (:playwright default,
16
+ # :selenium supported).
17
+ module DocScreenshotHelper
18
+ # Conditional screenshot capture. Enable with: SCREENSHOTS=true rspec spec/...
19
+ def doc_screenshot(filename)
20
+ return unless AutoPlatformGuide.screenshots_enabled?
21
+
22
+ sleep 0.5 # Allow time for animations to complete
23
+ save_screenshot(Rails.root.join(AutoPlatformGuide.configuration.screenshots_dir, "#{filename}.png")) # rubocop:disable Lint/Debugger
24
+ end
25
+ end
26
+
27
+ def self.setup_rspec(config)
28
+ # A contended machine (CI, or sibling spec:parallel workers) lets async Turbo
29
+ # Stream deliveries exceed a short wait. Only the slow path lengthens.
30
+ contended = ci? || ENV.key?('TEST_ENV_NUMBER')
31
+ max_wait_time = contended ? 10 : 5
32
+
33
+ screen_size = ENV.fetch('CAPYBARA_WINDOW_SIZE', '1920,900').split(',').map(&:to_i)
34
+ browser_options = driver_options(max_wait_time)
35
+
36
+ configure_capybara
37
+
38
+ Capybara.save_path = './log/capybara'
39
+
40
+ config.include DocScreenshotHelper, type: :system
41
+
42
+ # rack_test by default, for performance
43
+ config.before(type: :system) do
44
+ AutoPlatformGuide.prepare_system_example(self, max_wait_time, screen_size, browser_options)
45
+ end
46
+
47
+ config.around(:each, type: :system) do |example|
48
+ AutoPlatformGuide.with_forgery_protection(example)
49
+ end
50
+
51
+ config.before(type: :system, js: true) do
52
+ AutoPlatformGuide.prepare_js_example(self, screen_size, browser_options)
53
+ end
54
+ end
55
+
56
+ def self.prepare_system_example(example, max_wait_time, screen_size, browser_options)
57
+ Capybara.default_max_wait_time = max_wait_time
58
+
59
+ if screenshots_enabled?
60
+ drive_with_browser(example, screen_size:, options: browser_options)
61
+ else
62
+ example.driven_by(:rack_test)
63
+ end
64
+ end
65
+
66
+ def self.with_forgery_protection(example)
67
+ ActionController::Base.allow_forgery_protection = true
68
+ example.run
69
+ ensure
70
+ ActionController::Base.allow_forgery_protection = false
71
+ end
72
+
73
+ def self.prepare_js_example(example, screen_size, browser_options)
74
+ drive_with_browser(example, screen_size:, options: browser_options)
75
+ attach_browser_logging(example) if configuration.browser_driver == :playwright
76
+ end
77
+
78
+ def self.configure_capybara
79
+ Capybara.configure do |capybara|
80
+ capybara.server_host = ENV.fetch(
81
+ 'CAPYBARA_SERVER_HOST',
82
+ ci? ? IPSocket.getaddress(Socket.gethostname) : 'localhost'
83
+ )
84
+
85
+ # Different ports per parallel worker to avoid conflicts
86
+ base_port = ci? ? 3000 : 3500
87
+ port_offset = ENV['TEST_ENV_NUMBER'].to_i
88
+ capybara.server_port = base_port + port_offset
89
+
90
+ capybara.app_host = ENV.fetch('CAPYBARA_APP_HOST', "http://#{capybara.server_host}:#{capybara.server_port}")
91
+ capybara.raise_server_errors = false
92
+ end
93
+ end
94
+
95
+ def self.drive_with_browser(example, screen_size:, options:)
96
+ case configuration.browser_driver
97
+ when :playwright
98
+ example.driven_by(:playwright, screen_size:, options:)
99
+ when :selenium
100
+ example.driven_by(:selenium, using: :headless_chrome, screen_size:)
101
+ else
102
+ raise ArgumentError, "Unknown browser_driver: #{configuration.browser_driver.inspect}"
103
+ end
104
+ end
105
+
106
+ # Each example gets a fresh browser context, so web storage cannot leak between them.
107
+ def self.attach_browser_logging(example)
108
+ example.page.driver.with_playwright_page do |playwright_page|
109
+ playwright_page.on('console', lambda { |message|
110
+ Rails.logger.error("[BROWSER][#{message.type}] #{message.text}") if message.type == 'error'
111
+ })
112
+ playwright_page.on('pageerror', lambda { |error|
113
+ Rails.logger.error("[BROWSER][pageerror] #{error.message}")
114
+ })
115
+ end
116
+ end
117
+
118
+ def self.driver_options(max_wait_time)
119
+ return {} unless configuration.browser_driver == :playwright
120
+
121
+ # Keystrokes would otherwise inherit Playwright's own 30s timeout; navigation keeps a
122
+ # long budget because a click that waits for one is bounded by the action timeout.
123
+ { default_timeout: max_wait_time, default_navigation_timeout: 30 }
124
+ end
125
+
126
+ def self.ci?
127
+ ENV['CI'].present?
128
+ end
129
+
130
+ def self.screenshots_enabled?
131
+ ENV['SCREENSHOTS'] == 'true'
132
+ end
133
+ end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AutoPlatformGuide
4
+ # A system spec file carrying `# = Heading` doc comments and
5
+ # `doc_screenshot('name')` calls. Parses it once and exposes what the
6
+ # generator needs: headings with their body, screenshots, TOC directives and
7
+ # the doc title.
8
+ class SpecFile
9
+ # One `=` per heading level: `# = Title`, `# == Sub-title`.
10
+ HEADING = /^\s*#\s*(=+)\s*(.+?)\s*$/
11
+ # Exactly one space after `#` is the comment prefix; anything beyond it is
12
+ # content, so indented code inside fenced blocks survives.
13
+ COMMENT = /^\s*#\s?(.*?)\s*$/
14
+
15
+ # `@category` / `@order` control TOC placement only and are stripped from the
16
+ # body. `@order` is global across guides and categories sort by their
17
+ # smallest `@order`, so there is no category list to keep in sync.
18
+ CATEGORY_DIRECTIVE = /^\s*#\s*@category\s+(.+?)\s*$/
19
+ ORDER_DIRECTIVE = /^\s*#\s*@order\s+(\d+)\s*$/
20
+ DIRECTIVE = /^\s*#\s*@(?:category|order)\b/
21
+
22
+ def initialize(path)
23
+ @path = path
24
+ end
25
+
26
+ # True when the spec has at least one top-level doc comment.
27
+ def documented?
28
+ lines.any? { |line| line.match?(/^#\s*=/) }
29
+ end
30
+
31
+ # Comment blocks in source order: `{ level:, title:, content:, line_number: }`.
32
+ # A block runs until the next heading or the first non-comment line;
33
+ # `@category`/`@order` lines are TOC metadata, never body content.
34
+ def doc_comments
35
+ @doc_comments ||= parse_doc_comments
36
+ end
37
+
38
+ # `{ path:, line_number: }` for every `doc_screenshot('...')` call.
39
+ def screenshot_calls
40
+ @screenshot_calls ||= parse_screenshot_calls
41
+ end
42
+
43
+ # First `@category` / `@order` in the file, or nil.
44
+ def directives
45
+ @directives ||= { category: first_match(CATEGORY_DIRECTIVE), order: first_match(ORDER_DIRECTIVE)&.to_i }
46
+ end
47
+
48
+ # The top-level (`# =`, not `# ==`) heading, falling back to the file name.
49
+ def title
50
+ @title ||= lines.filter_map { |line| line.match(/^\s*#\s*=(?!=)\s*(.+?)\s*$/)&.[](1)&.strip }
51
+ .reject(&:empty?).first ||
52
+ File.basename(@path, '_spec.rb').split('_').map(&:capitalize).join(' ')
53
+ end
54
+
55
+ private
56
+
57
+ def lines
58
+ @lines ||= File.readlines(@path, encoding: 'UTF-8')
59
+ end
60
+
61
+ def first_match(pattern)
62
+ lines.filter_map { |line| line.match(pattern)&.[](1) }.first
63
+ end
64
+
65
+ def parse_doc_comments
66
+ blocks = []
67
+ current = nil
68
+
69
+ lines.each.with_index(1) do |line, number|
70
+ next if DIRECTIVE.match?(line)
71
+
72
+ if (heading = line.match(HEADING))
73
+ blocks << current if current
74
+ title = heading[2].strip
75
+ # A divider line like `# ==== ` has no title and is not a heading.
76
+ current = title.empty? ? nil : { level: heading[1].length, title:, content: [], line_number: number }
77
+ elsif current.nil?
78
+ next
79
+ elsif (comment = line.match(COMMENT))
80
+ current[:content] << comment[1]
81
+ else
82
+ blocks << current
83
+ current = nil
84
+ end
85
+ end
86
+
87
+ blocks << current if current
88
+ blocks
89
+ end
90
+
91
+ def parse_screenshot_calls
92
+ call = %r{doc_screenshot\(['"]([\w\-/]+)['"]\)}
93
+
94
+ lines.each.with_index(1).filter_map do |line, number|
95
+ path = line[call, 1]
96
+ { path:, line_number: number } if path
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AutoPlatformGuide
4
+ VERSION = '0.1.0'
5
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'auto_platform_guide/version'
4
+ require 'auto_platform_guide/configuration'
5
+ require 'auto_platform_guide/spec_file'
6
+ require 'auto_platform_guide/document'
7
+ require 'auto_platform_guide/generator'
8
+ require 'auto_platform_guide/docs_controller_actions'
9
+
10
+ module AutoPlatformGuide
11
+ def self.load_engine
12
+ require 'auto_platform_guide/engine' if defined?(Rails::Engine)
13
+ end
14
+ private_class_method :load_engine
15
+ end
16
+
17
+ AutoPlatformGuide.send(:load_engine)
metadata ADDED
@@ -0,0 +1,158 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: auto-platform-guide
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Mauricio Zaffari
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: actionpack
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '7.1'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '7.1'
26
+ - !ruby/object:Gem::Dependency
27
+ name: actionview
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '7.1'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '7.1'
40
+ - !ruby/object:Gem::Dependency
41
+ name: activemodel
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '7.1'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '7.1'
54
+ - !ruby/object:Gem::Dependency
55
+ name: capybara
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '3.40'
61
+ type: :runtime
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '3.40'
68
+ - !ruby/object:Gem::Dependency
69
+ name: railties
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '7.1'
75
+ type: :runtime
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: '7.1'
82
+ - !ruby/object:Gem::Dependency
83
+ name: redcarpet
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: '3.6'
89
+ type: :runtime
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - "~>"
94
+ - !ruby/object:Gem::Version
95
+ version: '3.6'
96
+ - !ruby/object:Gem::Dependency
97
+ name: rouge
98
+ requirement: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - "~>"
101
+ - !ruby/object:Gem::Version
102
+ version: '5.0'
103
+ type: :runtime
104
+ prerelease: false
105
+ version_requirements: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - "~>"
108
+ - !ruby/object:Gem::Version
109
+ version: '5.0'
110
+ description: Generator and spec helpers that turn doc comments and screenshots in
111
+ system specs into markdown platform guides, plus a markdown docs viewer for Rails
112
+ admin apps.
113
+ email:
114
+ - mauricio@develoz.com
115
+ executables: []
116
+ extensions: []
117
+ extra_rdoc_files: []
118
+ files:
119
+ - CHANGELOG.md
120
+ - LICENSE.txt
121
+ - README.md
122
+ - app/views/docs/show.html.erb
123
+ - lib/auto-platform-guide.rb
124
+ - lib/auto_platform_guide.rb
125
+ - lib/auto_platform_guide/configuration.rb
126
+ - lib/auto_platform_guide/docs_controller_actions.rb
127
+ - lib/auto_platform_guide/document.rb
128
+ - lib/auto_platform_guide/engine.rb
129
+ - lib/auto_platform_guide/generator.rb
130
+ - lib/auto_platform_guide/rake_tasks.rb
131
+ - lib/auto_platform_guide/rspec.rb
132
+ - lib/auto_platform_guide/spec_file.rb
133
+ - lib/auto_platform_guide/version.rb
134
+ homepage: https://github.com/mauriciozaffari/auto-platform-guide
135
+ licenses:
136
+ - MIT
137
+ metadata:
138
+ source_code_uri: https://github.com/mauriciozaffari/auto-platform-guide
139
+ changelog_uri: https://github.com/mauriciozaffari/auto-platform-guide/blob/main/CHANGELOG.md
140
+ rubygems_mfa_required: 'true'
141
+ rdoc_options: []
142
+ require_paths:
143
+ - lib
144
+ required_ruby_version: !ruby/object:Gem::Requirement
145
+ requirements:
146
+ - - ">="
147
+ - !ruby/object:Gem::Version
148
+ version: '3.2'
149
+ required_rubygems_version: !ruby/object:Gem::Requirement
150
+ requirements:
151
+ - - ">="
152
+ - !ruby/object:Gem::Version
153
+ version: '0'
154
+ requirements: []
155
+ rubygems_version: 4.0.16
156
+ specification_version: 4
157
+ summary: Generate in-app documentation from system specs.
158
+ test_files: []