docyard 0.0.1 → 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.
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "rack"
5
+
6
+ module Docyard
7
+ class RackApplication
8
+ def initialize(docs_path:, file_watcher:)
9
+ @docs_path = docs_path
10
+ @file_watcher = file_watcher
11
+ @router = Router.new(docs_path: docs_path)
12
+ @renderer = Renderer.new
13
+ @asset_handler = AssetHandler.new
14
+ end
15
+
16
+ def call(env)
17
+ handle_request(env)
18
+ end
19
+
20
+ private
21
+
22
+ attr_reader :docs_path, :file_watcher, :router, :renderer, :asset_handler
23
+
24
+ def handle_request(env)
25
+ path = env["PATH_INFO"]
26
+
27
+ return handle_reload_check(env) if path == "/_docyard/reload"
28
+ return asset_handler.serve(path) if path.start_with?("/assets/")
29
+
30
+ handle_documentation_request(path)
31
+ rescue StandardError => e
32
+ handle_error(e)
33
+ end
34
+
35
+ def handle_documentation_request(path)
36
+ file_path = router.resolve(path)
37
+ status = file_path ? 200 : 404
38
+ html = file_path ? renderer.render_file(file_path) : renderer.render_not_found
39
+
40
+ [status, { "Content-Type" => "text/html; charset=utf-8" }, [html]]
41
+ end
42
+
43
+ def handle_reload_check(env)
44
+ query = Rack::Utils.parse_query(env["QUERY_STRING"])
45
+ since = query["since"] ? Time.at(query["since"].to_f) : Time.now
46
+ reload_needed = file_watcher.changed_since?(since)
47
+
48
+ build_reload_response(reload_needed)
49
+ rescue StandardError => e
50
+ puts "[Docyard] Reload check error: #{e.message}"
51
+ build_reload_response(false)
52
+ end
53
+
54
+ def build_reload_response(reload_needed)
55
+ response_body = { reload: reload_needed, timestamp: Time.now.to_f }.to_json
56
+ [200, { "Content-Type" => "application/json" }, [response_body]]
57
+ end
58
+
59
+ def handle_error(error)
60
+ [500, { "Content-Type" => "text/html; charset=utf-8" }, [renderer.render_server_error(error)]]
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "erb"
4
+
5
+ module Docyard
6
+ class Renderer
7
+ LAYOUTS_PATH = File.join(__dir__, "templates", "layouts")
8
+ ERRORS_PATH = File.join(__dir__, "templates", "errors")
9
+
10
+ attr_reader :layout_path
11
+
12
+ def initialize(layout: "default")
13
+ @layout_path = File.join(LAYOUTS_PATH, "#{layout}.html.erb")
14
+ end
15
+
16
+ def render_file(file_path)
17
+ markdown_content = File.read(file_path)
18
+ markdown = Markdown.new(markdown_content)
19
+
20
+ html_content = strip_md_from_links(markdown.html)
21
+
22
+ render(
23
+ content: html_content,
24
+ page_title: markdown.title || "Documentation"
25
+ )
26
+ end
27
+
28
+ def render(content:, page_title: "Documentation")
29
+ template = File.read(layout_path)
30
+
31
+ @content = content
32
+ @page_title = page_title
33
+
34
+ ERB.new(template).result(binding)
35
+ end
36
+
37
+ def render_not_found
38
+ render_error_template(404)
39
+ end
40
+
41
+ def render_server_error(error)
42
+ @error_message = error.message
43
+ @backtrace = error.backtrace.join("\n")
44
+ render_error_template(500)
45
+ end
46
+
47
+ def render_error_template(status)
48
+ error_template_path = File.join(ERRORS_PATH, "#{status}.html.erb")
49
+ template = File.read(error_template_path)
50
+ ERB.new(template).result(binding)
51
+ end
52
+
53
+ private
54
+
55
+ def strip_md_from_links(html)
56
+ html.gsub(/href="([^"]+)\.md"/, 'href="\1"')
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docyard
4
+ class Router
5
+ attr_reader :docs_path
6
+
7
+ def initialize(docs_path:)
8
+ @docs_path = docs_path
9
+ end
10
+
11
+ def resolve(request_path)
12
+ clean_path = request_path.delete_prefix("/")
13
+ clean_path = "index" if clean_path.empty?
14
+
15
+ clean_path = clean_path.delete_suffix(".md")
16
+
17
+ file_path = File.join(docs_path, "#{clean_path}.md")
18
+ return file_path if File.file?(file_path)
19
+
20
+ index_path = File.join(docs_path, clean_path, "index.md")
21
+ return index_path if File.file?(index_path)
22
+
23
+ nil
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "webrick"
4
+ require "stringio"
5
+ require_relative "file_watcher"
6
+ require_relative "rack_application"
7
+
8
+ module Docyard
9
+ class Server
10
+ DEFAULT_PORT = 4200
11
+ DEFAULT_HOST = "localhost"
12
+
13
+ attr_reader :port, :host, :docs_path
14
+
15
+ def initialize(port: DEFAULT_PORT, host: DEFAULT_HOST, docs_path: "docs")
16
+ @port = port
17
+ @host = host
18
+ @docs_path = docs_path
19
+ @file_watcher = FileWatcher.new(File.expand_path(docs_path))
20
+ @app = RackApplication.new(
21
+ docs_path: File.expand_path(docs_path),
22
+ file_watcher: @file_watcher
23
+ )
24
+ end
25
+
26
+ def start
27
+ validate_docs_directory!
28
+ print_server_info
29
+ @file_watcher.start
30
+
31
+ http_server.mount_proc("/") { |req, res| handle_request(req, res) }
32
+ trap("INT") { shutdown_server }
33
+
34
+ http_server.start
35
+ @file_watcher.stop
36
+ end
37
+
38
+ private
39
+
40
+ def validate_docs_directory!
41
+ return if File.directory?(docs_path)
42
+
43
+ abort "Error: #{docs_path}/ directory not found.\n" \
44
+ "Run `docyard init` first to create the docs structure."
45
+ end
46
+
47
+ def print_server_info
48
+ puts "Starting Docyard server..."
49
+ puts "=> Serving docs from: #{docs_path}/"
50
+ puts "=> Running at: http://#{host}:#{port}"
51
+ puts "=> Press Ctrl+C to stop\n"
52
+ end
53
+
54
+ def shutdown_server
55
+ puts "\nShutting down server..."
56
+ http_server.shutdown
57
+ end
58
+
59
+ def http_server
60
+ @http_server ||= WEBrick::HTTPServer.new(
61
+ Port: port,
62
+ BindAddress: host,
63
+ AccessLog: [],
64
+ Logger: WEBrick::Log.new(File::NULL)
65
+ )
66
+ end
67
+
68
+ def handle_request(req, res)
69
+ env = build_rack_env(req)
70
+ status, headers, body = @app.call(env)
71
+
72
+ res.status = status
73
+ headers.each { |key, value| res[key] = value }
74
+ body.each { |chunk| res.body << chunk }
75
+ end
76
+
77
+ def build_rack_env(req)
78
+ {
79
+ "REQUEST_METHOD" => req.request_method,
80
+ "PATH_INFO" => req.path,
81
+ "QUERY_STRING" => req.query_string || "",
82
+ "SERVER_NAME" => req.host,
83
+ "SERVER_PORT" => req.port.to_s,
84
+ "rack.version" => Rack::VERSION,
85
+ "rack.url_scheme" => "http",
86
+ "rack.input" => StringIO.new,
87
+ "rack.errors" => $stderr
88
+ }
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,4 @@
1
+ body {
2
+ margin: 0;
3
+ font-family: system-ui, -apple-system, sans-serif;
4
+ }
@@ -0,0 +1,116 @@
1
+ .highlight table td { padding: 5px; }
2
+ .highlight table pre { margin: 0; }
3
+ .highlight, .highlight .w {
4
+ color: #24292f;
5
+ background-color: #f6f8fa;
6
+ }
7
+ .highlight .k, .highlight .kd, .highlight .kn, .highlight .kp, .highlight .kr, .highlight .kt, .highlight .kv {
8
+ color: #cf222e;
9
+ }
10
+ .highlight .gr {
11
+ color: #f6f8fa;
12
+ }
13
+ .highlight .gd {
14
+ color: #82071e;
15
+ background-color: #ffebe9;
16
+ }
17
+ .highlight .nb {
18
+ color: #953800;
19
+ }
20
+ .highlight .nc {
21
+ color: #953800;
22
+ }
23
+ .highlight .no {
24
+ color: #953800;
25
+ }
26
+ .highlight .nn {
27
+ color: #953800;
28
+ }
29
+ .highlight .sr {
30
+ color: #116329;
31
+ }
32
+ .highlight .na {
33
+ color: #116329;
34
+ }
35
+ .highlight .nt {
36
+ color: #116329;
37
+ }
38
+ .highlight .gi {
39
+ color: #116329;
40
+ background-color: #dafbe1;
41
+ }
42
+ .highlight .ges {
43
+ font-weight: bold;
44
+ font-style: italic;
45
+ }
46
+ .highlight .kc {
47
+ color: #0550ae;
48
+ }
49
+ .highlight .l, .highlight .ld, .highlight .m, .highlight .mb, .highlight .mf, .highlight .mh, .highlight .mi, .highlight .il, .highlight .mo, .highlight .mx {
50
+ color: #0550ae;
51
+ }
52
+ .highlight .sb {
53
+ color: #0550ae;
54
+ }
55
+ .highlight .bp {
56
+ color: #0550ae;
57
+ }
58
+ .highlight .ne {
59
+ color: #0550ae;
60
+ }
61
+ .highlight .nl {
62
+ color: #0550ae;
63
+ }
64
+ .highlight .py {
65
+ color: #0550ae;
66
+ }
67
+ .highlight .nv, .highlight .vc, .highlight .vg, .highlight .vi, .highlight .vm {
68
+ color: #0550ae;
69
+ }
70
+ .highlight .o, .highlight .ow {
71
+ color: #0550ae;
72
+ }
73
+ .highlight .gh {
74
+ color: #0550ae;
75
+ font-weight: bold;
76
+ }
77
+ .highlight .gu {
78
+ color: #0550ae;
79
+ font-weight: bold;
80
+ }
81
+ .highlight .s, .highlight .sa, .highlight .sc, .highlight .dl, .highlight .sd, .highlight .s2, .highlight .se, .highlight .sh, .highlight .sx, .highlight .s1, .highlight .ss {
82
+ color: #0a3069;
83
+ }
84
+ .highlight .nd {
85
+ color: #8250df;
86
+ }
87
+ .highlight .nf, .highlight .fm {
88
+ color: #8250df;
89
+ }
90
+ .highlight .err {
91
+ color: #f6f8fa;
92
+ background-color: #82071e;
93
+ }
94
+ .highlight .c, .highlight .ch, .highlight .cd, .highlight .cm, .highlight .cp, .highlight .cpf, .highlight .c1, .highlight .cs {
95
+ color: #6e7781;
96
+ }
97
+ .highlight .gl {
98
+ color: #6e7781;
99
+ }
100
+ .highlight .gt {
101
+ color: #6e7781;
102
+ }
103
+ .highlight .ni {
104
+ color: #24292f;
105
+ }
106
+ .highlight .si {
107
+ color: #24292f;
108
+ }
109
+ .highlight .ge {
110
+ color: #24292f;
111
+ font-style: italic;
112
+ }
113
+ .highlight .gs {
114
+ color: #24292f;
115
+ font-weight: bold;
116
+ }
@@ -0,0 +1,98 @@
1
+ (function () {
2
+ let lastCheck = Date.now() / 1000;
3
+ let basePollInterval = 500;
4
+ let currentPollInterval = basePollInterval;
5
+ let isReloading = false;
6
+ let consecutiveFailures = 0;
7
+ let serverWasDown = false;
8
+ let timeoutId = null;
9
+
10
+ async function fetchWithTimeout(resource, options = {}) {
11
+ const { timeout = 5000 } = options;
12
+
13
+ const controller = new AbortController();
14
+ const id = setTimeout(() => controller.abort(), timeout);
15
+
16
+ try {
17
+ const response = await fetch(resource, {
18
+ ...options,
19
+ signal: controller.signal
20
+ });
21
+ clearTimeout(id);
22
+ return response;
23
+ } catch (error) {
24
+ clearTimeout(id);
25
+ throw error;
26
+ }
27
+ }
28
+
29
+ async function checkForChanges() {
30
+ if (isReloading) return;
31
+
32
+ try {
33
+ const response = await fetchWithTimeout(`/_docyard/reload?since=${lastCheck}`, { timeout: 3000 });
34
+ const data = await response.json();
35
+ lastCheck = data.timestamp;
36
+
37
+ if (consecutiveFailures > 0) {
38
+ consecutiveFailures = 0;
39
+ currentPollInterval = basePollInterval;
40
+ if (serverWasDown) {
41
+ console.log('[Docyard] Server reconnected');
42
+ serverWasDown = false;
43
+ }
44
+ }
45
+
46
+ if (data.reload && !isReloading) {
47
+ isReloading = true;
48
+ console.log("[Docyard] Changes detected, hot reloading...");
49
+
50
+ try {
51
+ const resp = await fetchWithTimeout(window.location.href, { timeout: 5000 });
52
+ const html = await resp.text();
53
+ const parser = new DOMParser();
54
+ const newDoc = parser.parseFromString(html, 'text/html');
55
+
56
+ const oldMain = document.querySelector('main');
57
+ const newMain = newDoc.querySelector('main');
58
+
59
+ if (oldMain && newMain) {
60
+ oldMain.innerHTML = newMain.innerHTML;
61
+ console.log('[Docyard] Content updated via hot reload');
62
+ } else {
63
+ console.log('[Docyard] Layout changed, full reload required');
64
+ window.location.reload();
65
+ return;
66
+ }
67
+
68
+ isReloading = false;
69
+ } catch (error) {
70
+ console.error('[Docyard] Hot reload failed:', error);
71
+ console.log('[Docyard] Falling back to full reload');
72
+ window.location.reload();
73
+ }
74
+ }
75
+ } catch (error) {
76
+ consecutiveFailures++;
77
+
78
+ if (consecutiveFailures === 1) {
79
+ console.log('[Docyard] Server disconnected - live reload paused');
80
+ serverWasDown = true;
81
+ }
82
+
83
+ if (consecutiveFailures >= 3) {
84
+ console.log('[Docyard] Stopped polling. Refresh page when server restarts.');
85
+ return;
86
+ }
87
+
88
+ currentPollInterval = Math.min(basePollInterval * Math.pow(2, consecutiveFailures - 1), 5000);
89
+
90
+ isReloading = false;
91
+ }
92
+
93
+ timeoutId = setTimeout(checkForChanges, currentPollInterval);
94
+ }
95
+
96
+ checkForChanges();
97
+ console.log("[Docyard] Hot reload initialized");
98
+ })();
@@ -0,0 +1 @@
1
+ console.log('Docyard loaded');
@@ -0,0 +1,16 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>404 - Page Not Found</title>
7
+ <link rel="stylesheet" href="/assets/css/main.css">
8
+ </head>
9
+ <body>
10
+ <main>
11
+ <h1>404 - Page Not Found</h1>
12
+ <p>The page you're looking for doesn't exist.</p>
13
+ <p><a href="/">Go back home</a></p>
14
+ </main>
15
+ </body>
16
+ </html>
@@ -0,0 +1,25 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>500 - Server Error</title>
7
+ <link rel="stylesheet" href="/assets/css/main.css">
8
+ </head>
9
+ <body>
10
+ <main>
11
+ <h1>500 - Internal Server Error</h1>
12
+ <p>Something went wrong.</p>
13
+
14
+ <% if @error_message %>
15
+ <h3>Error Details:</h3>
16
+ <pre><%= @error_message %></pre>
17
+ <% end %>
18
+
19
+ <% if @backtrace %>
20
+ <h3>Backtrace:</h3>
21
+ <pre><%= @backtrace %></pre>
22
+ <% end %>
23
+ </main>
24
+ </body>
25
+ </html>
@@ -0,0 +1,29 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title><%= @page_title || 'Documentation' %></title>
7
+ <link rel="stylesheet" href="/assets/css/main.css">
8
+ <link rel="stylesheet" href="/assets/css/syntax.css">
9
+ </head>
10
+ <body>
11
+ <header>
12
+ <h1>Documentation</h1>
13
+ </header>
14
+
15
+ <nav>
16
+ <!-- Sidebar will go here -->
17
+ </nav>
18
+
19
+ <main>
20
+ <%= @content %>
21
+ </main>
22
+
23
+ <footer>
24
+ <p>Built with Docyard</p>
25
+ </footer>
26
+
27
+ <script src="/assets/js/reload.js"></script>
28
+ </body>
29
+ </html>
@@ -0,0 +1,40 @@
1
+ ---
2
+ title: Getting Started
3
+ description: Learn how to use Docyard
4
+ ---
5
+
6
+ # Getting Started
7
+
8
+ Welcome! This guide will help you get started with Docyard.
9
+
10
+ ## Writing Documentation
11
+
12
+ Just write markdown files in the `docs/` folder.
13
+
14
+ ### Frontmatter
15
+
16
+ Each markdown file can have YAML frontmatter:
17
+
18
+ ```yaml
19
+ ---
20
+ title: Page Title
21
+ description: Page description
22
+ ---
23
+
24
+ Markdown Syntax
25
+
26
+ Docyard supports all standard markdown:
27
+
28
+ - Bold text
29
+ - Italic text
30
+ - Code snippets
31
+ - Links and images
32
+ - Code blocks with syntax highlighting
33
+
34
+ Next Steps
35
+
36
+ - Add more markdown files to docs/
37
+ - Organize files into folders
38
+ - Customize your site with docyard.yml
39
+
40
+ Happy documenting! 📚
@@ -0,0 +1,22 @@
1
+ ---
2
+ title: Home
3
+ description: Welcome to your documentation site
4
+ ---
5
+
6
+ # Welcome to Docyard!
7
+
8
+ This is your documentation homepage. Edit this file to customize it.
9
+
10
+ ## What is Docyard?
11
+
12
+ Docyard is a beautiful, zero-config documentation generator built with Ruby.
13
+
14
+ ## Quick Start
15
+
16
+ 1. Edit the markdown files in `docs/`
17
+ 2. Run `docyard serve` to preview your site
18
+ 3. Run `docyard build` to generate static HTML
19
+
20
+ ## Learn More
21
+
22
+ Check out the [Getting Started](getting-started.md) guide.
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Docyard
4
- VERSION = "0.0.1"
4
+ VERSION = "0.1.0"
5
5
  end
data/lib/docyard.rb CHANGED
@@ -1,8 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "docyard/version"
4
+ require_relative "docyard/cli"
5
+ require_relative "docyard/initializer"
6
+ require_relative "docyard/markdown"
7
+ require_relative "docyard/router"
8
+ require_relative "docyard/renderer"
9
+ require_relative "docyard/asset_handler"
10
+ require_relative "docyard/server"
4
11
 
5
12
  module Docyard
6
13
  class Error < StandardError; end
7
- # Your code goes here...
8
14
  end