elder_docs 0.1.3 → 0.1.5

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 20f0c58e7d2f5a6497d153f05cc9127730b8897b34a0a9ac8a42959e49f263f4
4
- data.tar.gz: e9230ebcf9b18e2e3b2809dd2cd7d2880e6fe346e0d251ea32f3011a579a42f9
3
+ metadata.gz: 6ec656c7eb8b5c1d06212d3004fbf785b6e4d1a4a5364d66dcf2f637447465c4
4
+ data.tar.gz: 691d4f247a2afa096ba57510fc515e926b44fff596fc9c56982e8682bdfe76ee
5
5
  SHA512:
6
- metadata.gz: f4b30353dd92332b3a017ca6df2598daeaf96ca4c5ce67b2d0100eaeda4d26b61dd949168440fa8aa60bd3914c81fbfa4c8fed0a303b2e0bbbda16bef3b8fa03
7
- data.tar.gz: e13f040aa72ecea558922a99c678975b324f5860d5e8bd035268b6337eee23bb4ef17d879388f7ce5edaf71fa2e01a53ec95874d9eb647e013bf31adbc3f048a
6
+ metadata.gz: 1601af0b736fced56e45a3a44a3727d63114c34a28378c83ae4db4b49febd65e3756c03e221358c35847012760f0e0f6e5c2943c7ffebc8d8dcbb78a28ff8e96
7
+ data.tar.gz: dd5209f2fbbed2e74a4a75cc10e2b43920640ad975e0d38d0b492714a747526e3624e4918cea5ba3b8016e615cf2428f36080a7f5ad320bb9ad409c968bf9c0a
data/README.md CHANGED
@@ -5,10 +5,11 @@ Interactive API documentation for Rails. Convert your OpenAPI spec into a beauti
5
5
  ## Features
6
6
 
7
7
  - 🚀 **Interactive API Explorer** - Test endpoints directly from docs
8
- - 🎨 **Customizable UI** - Web-based configurator at `/docs/ui`
8
+ - 🎨 **Customizable UI** - Configure via `elderdocs.yml`
9
9
  - 📖 **Guides & Articles** - Add custom documentation
10
10
  - 🔐 **Multiple Auth Types** - Bearer, API Key, Basic, OAuth2
11
- - ⚡ **Zero Config** - Works out of the box
11
+ - ⚡ **Zero Compilation** - Edit JSON files, changes appear instantly!
12
+ - 📋 **Copy cURL** - One-click copy of API requests
12
13
 
13
14
  ## Installation
14
15
 
@@ -65,13 +66,16 @@ Create `articles.json`:
65
66
  ]
66
67
  ```
67
68
 
68
- ### 3. Generate docs
69
+ ### 3. Build frontend (one-time setup)
69
70
 
70
71
  ```bash
71
72
  bundle exec elderdocs deploy
72
73
  ```
73
74
 
74
- This builds the SPA into `public/elderdocs` (configurable via `output_path`) so the assets live alongside your application code.
75
+ This builds the frontend SPA into `public/elderdocs` (configurable via `output_path`).
76
+
77
+ **Note:** This is a one-time build. After this, you can edit `definitions.json` and `articles.json` - changes appear instantly without rebuilding!
78
+
75
79
  ### 4. Mount in routes
76
80
 
77
81
  ```ruby
@@ -83,11 +87,15 @@ mount ElderDocs::Engine, at: '/docs'
83
87
 
84
88
  Open `http://localhost:3000/docs` 🎉
85
89
 
86
- ## Customize UI
90
+ ## Dynamic Updates
91
+
92
+ **No compilation needed!** After the initial build:
87
93
 
88
- Visit `/docs/ui` to customize fonts, colors, and styling with a visual editor.
94
+ - ✏️ Edit `definitions.json` Changes appear immediately
95
+ - ✏️ Edit `articles.json` → Changes appear immediately
96
+ - ✏️ Edit `elderdocs.yml` → Restart Rails server to see UI changes
89
97
 
90
- **Default password:** `admin` (or set `admin_password` in `elderdocs.yml`)
98
+ The frontend fetches data dynamically from your JSON files at runtime.
91
99
 
92
100
  ## Configuration
93
101
 
@@ -111,7 +119,7 @@ auth_types:
111
119
  - basic
112
120
  - oauth2
113
121
 
114
- # UI customization (or use /docs/ui)
122
+ # UI customization
115
123
  ui:
116
124
  font_heading: 'Syne'
117
125
  font_body: 'IBM Plex Sans'
@@ -122,8 +130,9 @@ ui:
122
130
  surface: '#ffffff'
123
131
  corner_radius: '0px'
124
132
 
125
- # Admin password for /docs/ui
126
- admin_password: your-secure-password
133
+ # Custom file paths (optional, defaults to definitions.json and articles.json)
134
+ definitions_file: definitions.json
135
+ articles_file: articles.json
127
136
 
128
137
  # Where to write generated assets (relative paths are resolved from Rails.root)
129
138
  output_path: ./public/elderdocs
@@ -8,9 +8,9 @@ mount_path: /docs
8
8
  # Output directory for generated assets (relative to your Rails root)
9
9
  output_path: ./public/elderdocs
10
10
 
11
- # Admin password for UI configuration page (/docs/ui)
12
- # Default: 'admin' (or set via ELDERDOCS_ADMIN_PASSWORD environment variable)
13
- # admin_password: your-secure-password-here
11
+ # Custom file paths (optional, defaults to definitions.json and articles.json in Rails root)
12
+ # definitions_file: definitions.json
13
+ # articles_file: articles.json
14
14
 
15
15
  # Default API server URL (overrides OpenAPI servers if set)
16
16
  # Leave empty to use the first server from your OpenAPI spec
data/frontend/src/App.jsx CHANGED
@@ -16,20 +16,35 @@ function App() {
16
16
  const ui = data.ui_config
17
17
 
18
18
  if (ui.colors) {
19
- if (ui.colors.primary) root.style.setProperty('--bd-primary', ui.colors.primary)
19
+ // Map config colors to CSS variable names used in index.css
20
+ if (ui.colors.primary) {
21
+ root.style.setProperty('--bd-yellow', ui.colors.primary)
22
+ root.style.setProperty('--bd-primary', ui.colors.primary) // Keep for compatibility
23
+ }
20
24
  if (ui.colors.secondary) {
21
- root.style.setProperty('--bd-secondary', ui.colors.secondary)
25
+ root.style.setProperty('--bd-charcoal', ui.colors.secondary)
26
+ root.style.setProperty('--bd-border', ui.colors.secondary)
27
+ root.style.setProperty('--bd-ink', ui.colors.secondary)
28
+ root.style.setProperty('--bd-secondary', ui.colors.secondary) // Keep for compatibility
29
+ }
30
+ if (ui.colors.background) {
31
+ root.style.setProperty('--bd-white', ui.colors.background)
32
+ root.style.setProperty('--bd-background', ui.colors.background) // Keep for compatibility
33
+ }
34
+ if (ui.colors.surface) {
35
+ root.style.setProperty('--bd-panel', ui.colors.surface)
36
+ root.style.setProperty('--bd-surface', ui.colors.surface) // Keep for compatibility
22
37
  }
23
- if (ui.colors.background) root.style.setProperty('--bd-background', ui.colors.background)
24
- if (ui.colors.surface) root.style.setProperty('--bd-surface', ui.colors.surface)
25
38
  }
26
39
 
27
40
  if (ui.corner_radius) {
28
- root.style.setProperty('--bd-corner-radius', ui.corner_radius)
41
+ root.style.setProperty('--bd-radius', ui.corner_radius)
42
+ root.style.setProperty('--bd-corner-radius', ui.corner_radius) // Keep for compatibility
29
43
  }
30
44
 
31
45
  if (ui.font_heading) {
32
- root.style.setProperty('--bd-font-heading', `'${ui.font_heading}', sans-serif`)
46
+ root.style.setProperty('--font-heading', `'${ui.font_heading}', sans-serif`)
47
+ root.style.setProperty('--bd-font-heading', `'${ui.font_heading}', sans-serif`) // Keep for compatibility
33
48
  // Dynamically load font
34
49
  const link = document.createElement('link')
35
50
  link.href = `https://fonts.googleapis.com/css2?family=${ui.font_heading.replace(/\s/g, '+')}:wght@500;600;700&display=swap`
@@ -40,7 +55,8 @@ function App() {
40
55
  }
41
56
 
42
57
  if (ui.font_body) {
43
- root.style.setProperty('--bd-font-body', `'${ui.font_body}', sans-serif`)
58
+ root.style.setProperty('--font-body', `'${ui.font_body}', sans-serif`)
59
+ root.style.setProperty('--bd-font-body', `'${ui.font_body}', sans-serif`) // Keep for compatibility
44
60
  // Dynamically load font
45
61
  const link = document.createElement('link')
46
62
  link.href = `https://fonts.googleapis.com/css2?family=${ui.font_body.replace(/\s/g, '+')}:wght@400;500;600&display=swap`
@@ -65,49 +81,50 @@ function App() {
65
81
  }, [])
66
82
 
67
83
  useEffect(() => {
68
- const initializeData = (payload) => {
69
- setData(payload)
70
- setLoading(false)
71
-
72
- if (payload.openapi?.paths) {
73
- const firstPath = Object.keys(payload.openapi.paths)[0]
74
- const firstMethod = Object.keys(payload.openapi.paths[firstPath])[0]
75
- setSelectedItem({ type: 'endpoint', path: firstPath, method: firstMethod })
76
- } else if (payload.articles?.length > 0) {
77
- setSelectedItem({ type: 'article', id: payload.articles[0].id })
78
- }
79
- }
80
-
81
- const tryInitialize = () => {
82
- if (window.ElderDocsData) {
83
- initializeData(window.ElderDocsData)
84
- return true
85
- }
86
- return false
87
- }
88
-
89
- if (!tryInitialize()) {
90
- const handleDataLoaded = () => {
91
- if (window.ElderDocsData) {
92
- initializeData(window.ElderDocsData)
84
+ const loadData = async () => {
85
+ try {
86
+ // Fetch all data in parallel
87
+ const [definitionsRes, articlesRes, configRes] = await Promise.all([
88
+ fetch('/docs/api/definitions'),
89
+ fetch('/docs/api/articles'),
90
+ fetch('/docs/api/config')
91
+ ])
92
+
93
+ if (!definitionsRes.ok) {
94
+ throw new Error(`Failed to load definitions: ${definitionsRes.statusText}`)
93
95
  }
94
- }
95
-
96
- window.addEventListener('elderdocs:data_loaded', handleDataLoaded)
97
-
98
- // Fallback timeout so we don't wait forever
99
- const timeoutId = setTimeout(() => {
100
- if (!window.ElderDocsData) {
101
- console.error('ElderDocsData not found. Make sure data.js is loaded.')
102
- setLoading(false)
96
+
97
+ const openapi = await definitionsRes.json()
98
+ const articles = articlesRes.ok ? await articlesRes.json() : []
99
+ const config = configRes.ok ? await configRes.json() : {}
100
+
101
+ const payload = {
102
+ openapi,
103
+ articles,
104
+ api_server: config.api_server || '',
105
+ api_servers: config.api_servers || [],
106
+ auth_types: config.auth_types || ['bearer', 'api_key', 'basic', 'oauth2'],
107
+ ui_config: config.ui_config || {},
108
+ generated_at: new Date().toISOString()
103
109
  }
104
- }, 5000)
105
-
106
- return () => {
107
- window.removeEventListener('elderdocs:data_loaded', handleDataLoaded)
108
- clearTimeout(timeoutId)
110
+
111
+ setData(payload)
112
+ setLoading(false)
113
+
114
+ if (payload.openapi?.paths) {
115
+ const firstPath = Object.keys(payload.openapi.paths)[0]
116
+ const firstMethod = Object.keys(payload.openapi.paths[firstPath])[0]
117
+ setSelectedItem({ type: 'endpoint', path: firstPath, method: firstMethod })
118
+ } else if (payload.articles?.length > 0) {
119
+ setSelectedItem({ type: 'article', id: payload.articles[0].id })
120
+ }
121
+ } catch (error) {
122
+ console.error('Failed to load ElderDocs data:', error)
123
+ setLoading(false)
109
124
  }
110
125
  }
126
+
127
+ loadData()
111
128
  }, [])
112
129
 
113
130
  if (loading) {
@@ -11,6 +11,8 @@ const ApiExplorer = ({ data, selectedItem }) => {
11
11
  const [loading, setLoading] = useState(false)
12
12
  const [response, setResponse] = useState(null)
13
13
  const [isOpen, setIsOpen] = useState(true)
14
+ const [copied, setCopied] = useState(false)
15
+ const [showCurlPreview, setShowCurlPreview] = useState(false)
14
16
 
15
17
  const authTypes = data?.auth_types || ['bearer', 'api_key', 'basic', 'oauth2']
16
18
  const currentAuthType = authConfig?.type || 'bearer'
@@ -81,6 +83,85 @@ const ApiExplorer = ({ data, selectedItem }) => {
81
83
  return url
82
84
  }
83
85
 
86
+ const generateCurl = () => {
87
+ const url = buildUrl()
88
+ const method = selectedMethod.toUpperCase()
89
+ const curlParts = [`curl -X ${method}`]
90
+
91
+ // Add URL
92
+ curlParts.push(`"${url}"`)
93
+
94
+ // Add headers
95
+ const requestHeaders = {
96
+ 'Content-Type': 'application/json',
97
+ ...headers
98
+ }
99
+
100
+ // Apply authentication based on type
101
+ if (authConfig?.value) {
102
+ switch (authConfig.type) {
103
+ case 'bearer':
104
+ requestHeaders['Authorization'] = `Bearer ${authConfig.value}`
105
+ break
106
+ case 'api_key':
107
+ requestHeaders['X-API-Key'] = authConfig.value
108
+ break
109
+ case 'basic':
110
+ requestHeaders['Authorization'] = `Basic ${btoa(authConfig.value)}`
111
+ break
112
+ case 'oauth2':
113
+ requestHeaders['Authorization'] = `Bearer ${authConfig.value}`
114
+ break
115
+ }
116
+ }
117
+
118
+ // Add all headers to curl command
119
+ Object.entries(requestHeaders).forEach(([key, value]) => {
120
+ curlParts.push(`-H "${key}: ${value}"`)
121
+ })
122
+
123
+ // Add request body if present
124
+ if (['POST', 'PUT', 'PATCH'].includes(method) && body) {
125
+ try {
126
+ // Validate and format JSON
127
+ const jsonBody = JSON.parse(body)
128
+ const formattedBody = JSON.stringify(jsonBody)
129
+ curlParts.push(`-d '${formattedBody}'`)
130
+ } catch {
131
+ // If not valid JSON, add as-is
132
+ curlParts.push(`-d '${body}'`)
133
+ }
134
+ }
135
+
136
+ return curlParts.join(' \\\n ')
137
+ }
138
+
139
+ const copyCurl = async () => {
140
+ try {
141
+ const curlCommand = generateCurl()
142
+ await navigator.clipboard.writeText(curlCommand)
143
+ setCopied(true)
144
+ setTimeout(() => setCopied(false), 2000)
145
+ } catch (err) {
146
+ console.error('Failed to copy:', err)
147
+ // Fallback for older browsers
148
+ const textArea = document.createElement('textarea')
149
+ textArea.value = generateCurl()
150
+ textArea.style.position = 'fixed'
151
+ textArea.style.opacity = '0'
152
+ document.body.appendChild(textArea)
153
+ textArea.select()
154
+ try {
155
+ document.execCommand('copy')
156
+ setCopied(true)
157
+ setTimeout(() => setCopied(false), 2000)
158
+ } catch (fallbackErr) {
159
+ console.error('Fallback copy failed:', fallbackErr)
160
+ }
161
+ document.body.removeChild(textArea)
162
+ }
163
+ }
164
+
84
165
  const executeRequest = async () => {
85
166
  setLoading(true)
86
167
  setResponse(null)
@@ -282,14 +363,46 @@ const ApiExplorer = ({ data, selectedItem }) => {
282
363
  </div>
283
364
  )}
284
365
 
285
- <button
286
- onClick={executeRequest}
287
- disabled={loading}
288
- className="btn-primary w-full reveal"
289
- style={{ animationDelay: '300ms' }}
290
- >
291
- {loading ? 'Sending...' : 'Send Request'}
292
- </button>
366
+ <div className="reveal" style={{ animationDelay: '300ms' }}>
367
+ <button
368
+ onClick={() => setShowCurlPreview(!showCurlPreview)}
369
+ className="btn-secondary w-full mb-3 flex items-center justify-between"
370
+ >
371
+ <span className="flex items-center gap-2">
372
+ <span>💻</span>
373
+ <span>{showCurlPreview ? 'Hide' : 'Show'} cURL Command</span>
374
+ </span>
375
+ <span>{showCurlPreview ? '▲' : '▼'}</span>
376
+ </button>
377
+
378
+ {showCurlPreview && (
379
+ <div className="mb-3 surface border-2 border-black bg-white p-4">
380
+ <div className="flex items-center justify-between mb-2">
381
+ <p className="text-[0.65rem] uppercase tracking-[0.3em] text-black/60 font-bold">cURL Command</p>
382
+ <button
383
+ onClick={copyCurl}
384
+ className="text-xs btn-secondary px-2 py-1"
385
+ title="Copy cURL command"
386
+ >
387
+ {copied ? '✓ Copied!' : '📋 Copy'}
388
+ </button>
389
+ </div>
390
+ <pre className="text-xs font-mono text-black overflow-x-auto whitespace-pre-wrap break-words">
391
+ {generateCurl()}
392
+ </pre>
393
+ </div>
394
+ )}
395
+ </div>
396
+
397
+ <div className="reveal space-y-3" style={{ animationDelay: '320ms' }}>
398
+ <button
399
+ onClick={executeRequest}
400
+ disabled={loading}
401
+ className="btn-primary w-full"
402
+ >
403
+ {loading ? 'Sending...' : 'Send Request'}
404
+ </button>
405
+ </div>
293
406
 
294
407
  {response && (
295
408
  <div className="reveal" style={{ animationDelay: '320ms' }}>
@@ -100,16 +100,6 @@ const Sidebar = ({ data, activeView, setActiveView, selectedItem, setSelectedIte
100
100
  <div className="p-6 border-b-0 border-black/10 relative" style={{ borderBottomWidth: '3px' }}>
101
101
  <div className="pill text-[0.65rem] text-black mb-3 bg-white">ElderDocs</div>
102
102
  <h1 className="font-black text-3xl tracking-tight text-black uppercase font-['Syne']">API Space</h1>
103
- <div className="mt-4 mb-2">
104
- <a
105
- href="/docs/ui"
106
- target="_blank"
107
- rel="noopener noreferrer"
108
- className="text-xs text-black/60 hover:text-black underline font-medium"
109
- >
110
- ⚙️ Configure UI
111
- </a>
112
- </div>
113
103
  <div className="flex gap-2 mt-6">
114
104
  <button
115
105
  onClick={() => {
@@ -147,13 +147,37 @@ const UiConfigurator = () => {
147
147
 
148
148
  const applyConfig = (newConfig) => {
149
149
  const root = document.documentElement
150
- root.style.setProperty('--bd-primary', newConfig.colors.primary)
151
- root.style.setProperty('--bd-secondary', newConfig.colors.secondary)
152
- root.style.setProperty('--bd-background', newConfig.colors.background)
153
- root.style.setProperty('--bd-surface', newConfig.colors.surface)
154
- root.style.setProperty('--bd-corner-radius', newConfig.corner_radius)
155
- root.style.setProperty('--bd-font-heading', `'${newConfig.font_heading}', sans-serif`)
156
- root.style.setProperty('--bd-font-body', `'${newConfig.font_body}', sans-serif`)
150
+ // Map config colors to CSS variable names used in index.css
151
+ if (newConfig.colors.primary) {
152
+ root.style.setProperty('--bd-yellow', newConfig.colors.primary)
153
+ root.style.setProperty('--bd-primary', newConfig.colors.primary) // Keep for compatibility
154
+ }
155
+ if (newConfig.colors.secondary) {
156
+ root.style.setProperty('--bd-charcoal', newConfig.colors.secondary)
157
+ root.style.setProperty('--bd-border', newConfig.colors.secondary)
158
+ root.style.setProperty('--bd-ink', newConfig.colors.secondary)
159
+ root.style.setProperty('--bd-secondary', newConfig.colors.secondary) // Keep for compatibility
160
+ }
161
+ if (newConfig.colors.background) {
162
+ root.style.setProperty('--bd-white', newConfig.colors.background)
163
+ root.style.setProperty('--bd-background', newConfig.colors.background) // Keep for compatibility
164
+ }
165
+ if (newConfig.colors.surface) {
166
+ root.style.setProperty('--bd-panel', newConfig.colors.surface)
167
+ root.style.setProperty('--bd-surface', newConfig.colors.surface) // Keep for compatibility
168
+ }
169
+ if (newConfig.corner_radius) {
170
+ root.style.setProperty('--bd-radius', newConfig.corner_radius)
171
+ root.style.setProperty('--bd-corner-radius', newConfig.corner_radius) // Keep for compatibility
172
+ }
173
+ if (newConfig.font_heading) {
174
+ root.style.setProperty('--font-heading', `'${newConfig.font_heading}', sans-serif`)
175
+ root.style.setProperty('--bd-font-heading', `'${newConfig.font_heading}', sans-serif`) // Keep for compatibility
176
+ }
177
+ if (newConfig.font_body) {
178
+ root.style.setProperty('--font-body', `'${newConfig.font_body}', sans-serif`)
179
+ root.style.setProperty('--bd-font-body', `'${newConfig.font_body}', sans-serif`) // Keep for compatibility
180
+ }
157
181
 
158
182
  // Load fonts
159
183
  if (newConfig.font_heading) {
@@ -14,28 +14,21 @@ module ElderDocs
14
14
  method_option :force_build, type: :boolean, default: false, desc: 'Force rebuilding frontend assets'
15
15
 
16
16
  def deploy
17
- say '🚀 Starting ElderDocs deployment...', :green
18
-
19
- definitions_path = options[:definitions]
20
- articles_path = options[:articles]
21
-
22
- unless File.exist?(definitions_path)
23
- say "❌ Error: #{definitions_path} not found in current directory", :red
24
- exit 1
25
- end
26
-
27
- unless File.exist?(articles_path)
28
- say "⚠️ Warning: #{articles_path} not found. Creating empty articles file...", :yellow
29
- File.write(articles_path, [].to_json)
30
- end
17
+ say '🚀 Building ElderDocs frontend...', :green
18
+ say '📝 Note: definitions.json and articles.json are loaded dynamically at runtime', :cyan
19
+ say ' No need to rebuild when you update your API definitions!', :cyan
20
+ say ''
31
21
 
32
22
  output_path = File.expand_path(options[:output] || default_output_path, Dir.pwd)
33
-
34
23
  ElderDocs.config.output_path = output_path
35
24
 
25
+ # Reload config to get latest settings
26
+ ElderDocs.config.load_config_file
27
+
28
+ # Build frontend only (no data compilation needed)
36
29
  generator = Generator.new(
37
- definitions_path: definitions_path,
38
- articles_path: articles_path,
30
+ definitions_path: nil, # Not needed for dynamic mode
31
+ articles_path: nil, # Not needed for dynamic mode
39
32
  output_path: output_path,
40
33
  api_server: options[:api_server],
41
34
  skip_build: options[:skip_build],
@@ -43,9 +36,12 @@ module ElderDocs
43
36
  )
44
37
 
45
38
  begin
46
- generator.generate!
47
- say '✅ Documentation generated successfully!', :green
39
+ generator.build_frontend_only!
40
+ say '✅ Frontend built successfully!', :green
48
41
  say "📦 Assets placed in: #{generator.output_path}", :cyan
42
+ say ''
43
+ say '✨ Your documentation is now live!', :green
44
+ say ' Edit definitions.json and articles.json - changes appear instantly!', :cyan
49
45
  rescue Generator::ValidationError => e
50
46
  say "❌ Validation Error: #{e.message}", :red
51
47
  exit 1
@@ -5,7 +5,7 @@ require 'pathname'
5
5
 
6
6
  module ElderDocs
7
7
  class Config
8
- attr_accessor :mount_path, :api_server, :auth_types, :ui_config, :admin_password, :output_path
8
+ attr_accessor :mount_path, :api_server, :auth_types, :ui_config, :admin_password, :output_path, :definitions_file, :articles_file
9
9
 
10
10
  def initialize
11
11
  @mount_path = nil
@@ -15,6 +15,8 @@ module ElderDocs
15
15
  @admin_password = nil
16
16
  @api_servers = []
17
17
  @output_path = default_output_path
18
+ @definitions_file = nil
19
+ @articles_file = nil
18
20
  load_config_file
19
21
  end
20
22
 
@@ -40,6 +42,8 @@ module ElderDocs
40
42
  @auth_types = config['auth_types'] if config['auth_types']
41
43
  @ui_config = config['ui'] if config['ui'] # YAML uses 'ui' key, but we store as ui_config
42
44
  @admin_password = config['admin_password'] if config['admin_password']
45
+ @definitions_file = config['definitions_file'] if config['definitions_file']
46
+ @articles_file = config['articles_file'] if config['articles_file']
43
47
  if config['output_path']
44
48
  @output_path = File.expand_path(config['output_path'], config_dir)
45
49
  end
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module ElderDocs
6
+ class Engine
7
+ class ApiController < ActionController::API
8
+ include ActionController::MimeResponds
9
+
10
+ def definitions
11
+ definitions_path = find_definitions_file
12
+ if definitions_path && File.exist?(definitions_path)
13
+ render json: JSON.parse(File.read(definitions_path, encoding: 'UTF-8'))
14
+ else
15
+ render json: { error: 'definitions.json not found' }, status: :not_found
16
+ end
17
+ end
18
+
19
+ def articles
20
+ articles_path = find_articles_file
21
+ if articles_path && File.exist?(articles_path)
22
+ render json: JSON.parse(File.read(articles_path, encoding: 'UTF-8'))
23
+ else
24
+ render json: []
25
+ end
26
+ end
27
+
28
+ def config
29
+ # Reload config to get latest from elderdocs.yml
30
+ ElderDocs.config.load_config_file
31
+
32
+ config_data = {
33
+ api_server: ElderDocs.config.api_server,
34
+ api_servers: ElderDocs.config.api_servers || [],
35
+ auth_types: ElderDocs.config.auth_types || ['bearer', 'api_key', 'basic', 'oauth2'],
36
+ ui_config: ElderDocs.config.ui_config || {}
37
+ }
38
+
39
+ render json: config_data
40
+ end
41
+
42
+ private
43
+
44
+ def find_definitions_file
45
+ config = ElderDocs.config
46
+
47
+ # Check config file for definitions_file path
48
+ if config.respond_to?(:definitions_file) && config.definitions_file
49
+ config_path = find_config_file
50
+ if config_path
51
+ config_dir = File.dirname(config_path)
52
+ definitions_path = File.expand_path(config.definitions_file, config_dir)
53
+ return definitions_path if File.exist?(definitions_path)
54
+ end
55
+ end
56
+
57
+ # Fallback to standard locations
58
+ paths = []
59
+ if defined?(Rails) && Rails.root
60
+ paths << Rails.root.join('definitions.json').to_s
61
+ paths << Rails.root.join('elderdocs', 'definitions.json').to_s
62
+ end
63
+ paths << File.join(Dir.pwd, 'definitions.json')
64
+ paths << File.join(Dir.pwd, 'elderdocs', 'definitions.json')
65
+
66
+ paths.find { |path| File.exist?(path) }
67
+ end
68
+
69
+ def find_articles_file
70
+ config = ElderDocs.config
71
+
72
+ # Check config file for articles_file path
73
+ if config.respond_to?(:articles_file) && config.articles_file
74
+ config_path = find_config_file
75
+ if config_path
76
+ config_dir = File.dirname(config_path)
77
+ articles_path = File.expand_path(config.articles_file, config_dir)
78
+ return articles_path if File.exist?(articles_path)
79
+ end
80
+ end
81
+
82
+ # Fallback to standard locations
83
+ paths = []
84
+ if defined?(Rails) && Rails.root
85
+ paths << Rails.root.join('articles.json').to_s
86
+ paths << Rails.root.join('elderdocs', 'articles.json').to_s
87
+ end
88
+ paths << File.join(Dir.pwd, 'articles.json')
89
+ paths << File.join(Dir.pwd, 'elderdocs', 'articles.json')
90
+
91
+ paths.find { |path| File.exist?(path) } || create_empty_articles_file
92
+ end
93
+
94
+ def create_empty_articles_file
95
+ # Create empty articles.json if it doesn't exist
96
+ default_path = if defined?(Rails) && Rails.root
97
+ Rails.root.join('articles.json').to_s
98
+ else
99
+ File.join(Dir.pwd, 'articles.json')
100
+ end
101
+
102
+ File.write(default_path, [].to_json) unless File.exist?(default_path)
103
+ default_path
104
+ end
105
+
106
+ def find_config_file
107
+ config_paths = []
108
+ if defined?(Rails) && Rails.root
109
+ config_paths << Rails.root.join('elderdocs.yml').to_s
110
+ end
111
+ config_paths << File.join(Dir.pwd, 'elderdocs.yml')
112
+ config_paths.find { |path| File.exist?(path) }
113
+ end
114
+ end
115
+ end
116
+ end
117
+
@@ -8,15 +8,12 @@ module ElderDocs
8
8
 
9
9
  # Define routes inline
10
10
  routes do
11
- # UI Configuration endpoints (before catch-all)
12
- get '/ui', to: 'engine/ui_config#show'
13
- get '/ui/login', to: 'engine/ui_config#show_login'
14
- post '/ui/login', to: 'engine/ui_config#login'
15
- post '/ui/logout', to: 'engine/ui_config#logout'
16
- post '/ui/config', to: 'engine/ui_config#update'
11
+ # API endpoints for dynamic data
12
+ get '/api/definitions', to: 'engine/api#definitions'
13
+ get '/api/articles', to: 'engine/api#articles'
14
+ get '/api/config', to: 'engine/api#config'
17
15
 
18
- # Serve data.js explicitly before catch-all
19
- get '/data.js', to: 'engine/docs#show', defaults: { path: 'data.js' }
16
+ # Serve static assets
20
17
  root 'engine/docs#show', defaults: { path: 'index.html' }
21
18
  get '/*path', to: 'engine/docs#show', defaults: { path: 'index.html' }
22
19
  end
@@ -40,8 +37,8 @@ module ElderDocs
40
37
  end
41
38
  end
42
39
 
43
- # Load UI config controller
44
- require_relative 'engine/ui_config_controller'
40
+ # Load API controller
41
+ require_relative 'engine/api_controller'
45
42
 
46
43
  # Create a simple controller to serve the static files
47
44
  # Use API base to avoid CSRF protection
@@ -40,6 +40,20 @@ module ElderDocs
40
40
  end
41
41
  end
42
42
 
43
+ def build_frontend_only!
44
+ # Build frontend without data compilation (for dynamic mode)
45
+ # Check if assets already exist
46
+ assets_exist = File.exist?(File.join(output_path, 'index.html'))
47
+
48
+ if skip_build || (assets_exist && !force_build)
49
+ say '⏭️ Skipping frontend build (assets already exist)', :yellow
50
+ say '💡 Run with --force-build to rebuild', :cyan
51
+ else
52
+ build_frontend!
53
+ copy_assets!
54
+ end
55
+ end
56
+
43
57
  private
44
58
 
45
59
  def validate_definitions!
@@ -207,10 +221,12 @@ module ElderDocs
207
221
 
208
222
  frontend_dir = File.expand_path(frontend_dir)
209
223
 
210
- # Write compiled data to frontend public directory
211
- public_dir = File.join(frontend_dir, 'public')
212
- FileUtils.mkdir_p(public_dir)
213
- File.write(File.join(public_dir, 'data.js'), @compiled_data_js)
224
+ # Write compiled data to frontend public directory (only if in static mode)
225
+ if @compiled_data_js
226
+ public_dir = File.join(frontend_dir, 'public')
227
+ FileUtils.mkdir_p(public_dir)
228
+ File.write(File.join(public_dir, 'data.js'), @compiled_data_js)
229
+ end
214
230
 
215
231
  # Check if node_modules exists, if not, run npm install
216
232
  node_modules = File.join(frontend_dir, 'node_modules')
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ElderDocs
4
- VERSION = '0.1.3'
4
+ VERSION = '0.1.5'
5
5
  end
6
6
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: elder_docs
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.3
4
+ version: 0.1.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - ElderDocs
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-11-24 00:00:00.000000000 Z
11
+ date: 2025-11-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: thor
@@ -131,10 +131,11 @@ files:
131
131
  - lib/elder_docs/cli.rb
132
132
  - lib/elder_docs/config.rb
133
133
  - lib/elder_docs/engine.rb
134
+ - lib/elder_docs/engine/api_controller.rb
134
135
  - lib/elder_docs/engine/ui_config_controller.rb
135
136
  - lib/elder_docs/generator.rb
136
137
  - lib/elder_docs/version.rb
137
- homepage: https://github.com/yashdave31/elder_docs
138
+ homepage: https://github.com/yashdave31/elderdocs
138
139
  licenses:
139
140
  - MIT
140
141
  metadata: {}