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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a189b17059ce7d65d2bf10e3f920f193a1441cc2505a67b2acb9b9e4e99f5d94
4
- data.tar.gz: f08c2fc0fb50a8e8c4f52bda72f14b122e196ad82b474977111292115e56c80b
3
+ metadata.gz: 9b69315ac7baa43631fa2019a0ab8fb16d2b738efc622ad58c9dac3a4922b94d
4
+ data.tar.gz: f180d6145f4ae3ab99f41a91ea85c62d6824952782e33b7a91dfde612f68d9a3
5
5
  SHA512:
6
- metadata.gz: c0dc6551e43449d96f38fff9c0bf444bd8b233013be3af84690e2bf0c7db7795b75e9af9c02425e003a2ca2704aa7177452b90faebcc4bf599392afca17a34cc
7
- data.tar.gz: 12ea961b5b7acb9da1ba15eaac76b7e62f5bbb96bf8039f83cfe4ec0a002cf641f25333eda7824c7e76a90cbda3fd5c87eafe3ed7b3477b93f8b29e6fc92f546
6
+ metadata.gz: 7e8a176789932b8b09d7f07dbb1eb2789230e7a48b5b6bee09e2ff2cf29f780456d58231adf54933c902172feb858020ca1d1ef148bba4aaf5ec1a8a3d5d3795
7
+ data.tar.gz: 435ac4d482457cf029c1e98ca772d56291c7cbd810d6463a770d79b0e073925c0e790fe23bc21ad1f2386f715ba946d4520d5c25ffb6d8fc724731b3df61ba24
@@ -0,0 +1,31 @@
1
+ ---
2
+ name: Bug Report
3
+ about: Report a bug
4
+ title: ''
5
+ labels: bug
6
+ assignees: ''
7
+ ---
8
+
9
+ ## Description
10
+
11
+ <!-- Clear description of the bug -->
12
+
13
+ ## Steps to Reproduce
14
+
15
+ 1.
16
+ 2.
17
+ 3.
18
+
19
+ ## Expected Behavior
20
+
21
+ <!-- What should happen -->
22
+
23
+ ## Actual Behavior
24
+
25
+ <!-- What actually happens -->
26
+
27
+ ## Environment
28
+
29
+ - Ruby version: <!-- `ruby -v` -->
30
+ - Docyard version: <!-- `gem list docyard` -->
31
+ - OS: <!-- macOS, Linux, etc. -->
@@ -0,0 +1,19 @@
1
+ ---
2
+ name: Feature Request
3
+ about: Suggest a feature
4
+ title: ''
5
+ labels: enhancement
6
+ assignees: ''
7
+ ---
8
+
9
+ ## Description
10
+
11
+ <!-- What feature would you like? -->
12
+
13
+ ## Use Case
14
+
15
+ <!-- Why is this useful? When would you use it? -->
16
+
17
+ ## Implementation Ideas
18
+
19
+ <!-- Optional: How might this work? -->
@@ -0,0 +1,14 @@
1
+ ## Description
2
+
3
+ <!-- What does this PR do? -->
4
+
5
+ ## Changes
6
+
7
+ -
8
+ -
9
+
10
+ ## Checklist
11
+
12
+ - [ ] Tests added/updated
13
+ - [ ] CHANGELOG.md updated
14
+ - [ ] README.md updated (if needed)
@@ -0,0 +1,49 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ jobs:
10
+ test:
11
+ name: Ruby ${{ matrix.ruby }} - Test
12
+ runs-on: ubuntu-latest
13
+ strategy:
14
+ fail-fast: false
15
+ matrix:
16
+ ruby: ['3.2', '3.3', '3.4', 'head']
17
+ continue-on-error: ${{ matrix.ruby == 'head' }}
18
+
19
+ steps:
20
+ - name: Checkout code
21
+ uses: actions/checkout@v4
22
+
23
+ - name: Set up Ruby
24
+ uses: ruby/setup-ruby@v1
25
+ with:
26
+ ruby-version: ${{ matrix.ruby }}
27
+ bundler-cache: true
28
+
29
+ - name: Run tests
30
+ run: bundle exec rspec
31
+ env:
32
+ RUBYOPT: '-W0'
33
+
34
+ lint:
35
+ name: Rubocop
36
+ runs-on: ubuntu-latest
37
+
38
+ steps:
39
+ - name: Checkout code
40
+ uses: actions/checkout@v4
41
+
42
+ - name: Set up Ruby
43
+ uses: ruby/setup-ruby@v1
44
+ with:
45
+ ruby-version: '3.3'
46
+ bundler-cache: true
47
+
48
+ - name: Run rubocop
49
+ run: bundle exec rubocop
data/.rubocop.yml ADDED
@@ -0,0 +1,35 @@
1
+ plugins:
2
+ - rubocop-rspec
3
+ - rubocop-rake
4
+
5
+ AllCops:
6
+ NewCops: enable
7
+ TargetRubyVersion: 3.2
8
+ Exclude:
9
+ - 'vendor/**/*'
10
+ - 'spec/fixtures/**/*'
11
+ - 'tmp/**/*'
12
+
13
+ Metrics/BlockLength:
14
+ Exclude:
15
+ - 'spec/**/*'
16
+ - '*.gemspec'
17
+
18
+ RSpec/ExampleLength:
19
+ Max: 10
20
+
21
+ Metrics/ClassLength:
22
+ Max: 150
23
+
24
+ Metrics/MethodLength:
25
+ Max: 15
26
+
27
+ Style/Documentation:
28
+ Enabled: false
29
+
30
+ Layout/LineLength:
31
+ Max: 120
32
+
33
+ Style/StringLiterals:
34
+ Enabled: true
35
+ EnforcedStyle: double_quotes
data/CHANGELOG.md ADDED
@@ -0,0 +1,30 @@
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.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.1.0] - 2025-11-04
11
+
12
+ ### Added
13
+ - Hot reload
14
+ - GitHub Flavored Markdown support (tables, task lists, strikethrough)
15
+ - Syntax highlighting for 100+ languages via Rouge
16
+ - YAML frontmatter for page metadata
17
+ - Customizable 404/500 error templates
18
+ - CLI commands: `docyard init`, `docyard serve`
19
+ - File watcher for live reload
20
+ - Directory traversal protection in asset handler
21
+
22
+ ## [0.0.1] - 2025-11-03
23
+
24
+ ### Added
25
+ - Initial gem structure
26
+ - Project scaffolding
27
+
28
+ [Unreleased]: https://github.com/sanifhimani/docyard/compare/v0.1.0...HEAD
29
+ [0.1.0]: https://github.com/sanifhimani/docyard/compare/v0.0.1...v0.1.0
30
+ [0.0.1]: https://github.com/sanifhimani/docyard/releases/tag/v0.0.1
data/CONTRIBUTING.md ADDED
@@ -0,0 +1,55 @@
1
+ # Contributing
2
+
3
+ Thanks for wanting to contribute to Docyard!
4
+
5
+ ## Development Setup
6
+
7
+ ```bash
8
+ git clone https://github.com/YOUR_USERNAME/docyard.git
9
+ cd docyard
10
+ bundle install
11
+
12
+ # Run tests
13
+ bundle exec rspec
14
+ bundle exec rubocop
15
+
16
+ # Test locally
17
+ ./bin/docyard init
18
+ ./bin/docyard serve
19
+ ```
20
+
21
+ ## Pull Requests
22
+
23
+ 1. Fork the repo and create a branch from `main`
24
+ 2. Write tests for new features
25
+ 3. Run `bundle exec rspec && bundle exec rubocop`
26
+ 4. Update README/CHANGELOG if needed
27
+ 5. Use conventional commits: `feat:`, `fix:`, `refactor:`, `docs:`, `test:`
28
+
29
+ ## Architecture
30
+
31
+ ```
32
+ lib/docyard/
33
+ cli.rb # CLI
34
+ server.rb # Server lifecycle
35
+ rack_application.rb # HTTP handling
36
+ router.rb # URL → file mapping
37
+ renderer.rb # Markdown → HTML
38
+ markdown.rb # Parsing
39
+ file_watcher.rb # Live reload
40
+ asset_handler.rb # Static assets
41
+ ```
42
+
43
+ ## Code Style
44
+
45
+ - Maintain >85% test coverage
46
+ - Keep methods focused and readable
47
+ - Add tests for new features
48
+
49
+ ## Questions?
50
+
51
+ Open an issue.
52
+
53
+ ---
54
+
55
+ Thanks for contributing! 🚀
data/README.md CHANGED
@@ -1,14 +1,177 @@
1
1
  # Docyard
2
2
 
3
+ [![CI](https://github.com/sanifhimani/docyard/actions/workflows/ci.yml/badge.svg)](https://github.com/sanifhimani/docyard/actions/workflows/ci.yml)
4
+ [![Gem Version](https://badge.fury.io/rb/docyard.svg)](https://badge.fury.io/rb/docyard)
5
+
3
6
  > Documentation generator for Ruby
4
7
 
5
- ## What is this?
8
+ **Early development** - Core features work, but missing sidebar, search, and build command. See [roadmap](#roadmap).
9
+
10
+ ## Features
11
+
12
+ - **Hot reload** - Changes appear instantly while you write
13
+ - **GitHub Flavored Markdown** - Tables, task lists, strikethrough
14
+ - **Syntax highlighting** - 100+ languages via Rouge
15
+ - **YAML frontmatter** - Add metadata to your pages
16
+ - **Customizable error pages** - Make 404/500 pages your own
17
+
18
+ ## Quick Start
19
+
20
+ ```bash
21
+ # Install the gem
22
+ gem install docyard
23
+
24
+ # Create a new docs project
25
+ mkdir my-docs && cd my-docs
26
+ docyard init
6
27
 
7
- Building a modern documentation generator for Ruby. Zero config, convention over configuration.
28
+ # Start the dev server
29
+ docyard serve
30
+
31
+ # Visit http://localhost:4200
32
+ ```
8
33
 
9
34
  ## Installation
10
35
 
11
- Not published yet. Building in public - follow along!
36
+ Add to your Gemfile:
37
+
38
+ ```ruby
39
+ gem 'docyard'
40
+ ```
41
+
42
+ Or install directly:
43
+
44
+ ```bash
45
+ gem install docyard
46
+ ```
47
+
48
+ ## Usage
49
+
50
+ ### Initialize a New Project
51
+
52
+ ```bash
53
+ docyard init
54
+ ```
55
+
56
+ This creates:
57
+ ```
58
+ docs/
59
+ index.md # Home page
60
+ getting-started.md # Sample page
61
+ ```
62
+
63
+ ### Start Development Server
64
+
65
+ ```bash
66
+ docyard serve
67
+
68
+ # Custom port and host
69
+ docyard serve --port 3000 --host 0.0.0.0
70
+ ```
71
+
72
+ ### Writing Docs
73
+
74
+ Create markdown files in the `docs/` directory:
75
+
76
+ ```markdown
77
+ ---
78
+ title: Getting Started
79
+ ---
80
+
81
+ # Getting Started
82
+
83
+ \`\`\`ruby
84
+ class User
85
+ def initialize(name)
86
+ @name = name
87
+ end
88
+ end
89
+ \`\`\`
90
+ ```
91
+
92
+ ### Frontmatter
93
+
94
+ Add YAML frontmatter to customize page metadata:
95
+
96
+ ```yaml
97
+ ---
98
+ title: My Page Title
99
+ description: Page description
100
+ ---
101
+ ```
102
+
103
+ Currently supported:
104
+ - `title` - Page title (shown in `<title>` tag)
105
+
106
+ ### Linking Between Pages
107
+
108
+ Write links with `.md` extension, they'll be automatically cleaned:
109
+
110
+ ```markdown
111
+ [Getting Started](./getting-started.md) → /getting-started
112
+ [Guide](./guide/index.md) → /guide
113
+ ```
114
+
115
+ ### Directory Structure
116
+
117
+ ```
118
+ docs/
119
+ index.md # / (root)
120
+ getting-started.md # /getting-started
121
+ guide/
122
+ index.md # /guide
123
+ setup.md # /guide/setup
124
+ advanced.md # /guide/advanced
125
+ ```
126
+
127
+ ## Architecture
128
+
129
+ Clean separation of concerns:
130
+
131
+ ```
132
+ lib/docyard/
133
+ cli.rb # Command-line interface (Thor)
134
+ initializer.rb # Project scaffolding (init command)
135
+ server.rb # Server lifecycle (WEBrick, signals)
136
+ rack_application.rb # HTTP request handling (routing, rendering)
137
+ router.rb # URL → file path mapping
138
+ renderer.rb # Markdown → HTML conversion
139
+ markdown.rb # Markdown parsing & frontmatter extraction
140
+ file_watcher.rb # Live reload file monitoring
141
+ asset_handler.rb # Static asset serving
142
+ ```
143
+
144
+ Each class has a single, clear responsibility. Easy to test, easy to extend.
145
+
146
+ ## Development
147
+
148
+ ```bash
149
+ git clone https://github.com/sanifhimani/docyard.git
150
+ cd docyard
151
+ bundle install
152
+
153
+ # Run tests
154
+ bundle exec rspec
155
+
156
+ # Run linter
157
+ bundle exec rubocop
158
+
159
+ # Use local version
160
+ ./bin/docyard init
161
+ ./bin/docyard serve
162
+ ```
163
+
164
+ ## Roadmap
165
+
166
+ - Sidebar navigation
167
+ - Search
168
+ - Dark mode
169
+ - Build command
170
+ - Config file
171
+
172
+ ## Contributing
173
+
174
+ See [CONTRIBUTING.md](CONTRIBUTING.md)
12
175
 
13
176
  ## License
14
177
 
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docyard
4
+ class AssetHandler
5
+ ASSETS_PATH = File.join(__dir__, "templates", "assets")
6
+
7
+ CONTENT_TYPES = {
8
+ ".css" => "text/css; charset=utf-8",
9
+ ".js" => "application/javascript; charset=utf-8",
10
+ ".png" => "image/png",
11
+ ".jpg" => "image/jpeg",
12
+ ".jpeg" => "image/jpeg",
13
+ ".svg" => "image/svg+xml",
14
+ ".woff" => "font/woff2",
15
+ ".woff2" => "font/woff2",
16
+ ".ico" => "image/x-icon"
17
+ }.freeze
18
+
19
+ def serve(request_path)
20
+ asset_path = extract_asset_path(request_path)
21
+
22
+ return forbidden_response if directory_traversal?(asset_path)
23
+
24
+ file_path = build_file_path(asset_path)
25
+ return not_found_response unless File.file?(file_path)
26
+
27
+ serve_file(file_path)
28
+ end
29
+
30
+ private
31
+
32
+ def extract_asset_path(request_path)
33
+ request_path.delete_prefix("/assets/")
34
+ end
35
+
36
+ def directory_traversal?(path)
37
+ path.include?("..")
38
+ end
39
+
40
+ def build_file_path(asset_path)
41
+ File.join(ASSETS_PATH, asset_path)
42
+ end
43
+
44
+ def serve_file(file_path)
45
+ content = File.read(file_path)
46
+ content_type = detect_content_type(file_path)
47
+
48
+ [200, { "Content-Type" => content_type }, [content]]
49
+ end
50
+
51
+ def detect_content_type(file_path)
52
+ extension = File.extname(file_path)
53
+ CONTENT_TYPES.fetch(extension, "application/octet-stream")
54
+ end
55
+
56
+ def forbidden_response
57
+ [403, { "Content-Type" => "text/plain" }, ["403 Forbidden"]]
58
+ end
59
+
60
+ def not_found_response
61
+ [404, { "Content-Type" => "text/plain" }, ["404 Not Found"]]
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "thor"
4
+
5
+ module Docyard
6
+ class CLI < Thor
7
+ def self.exit_on_failure?
8
+ true
9
+ end
10
+
11
+ desc "version", "Show docyard version"
12
+ def version
13
+ puts "docyard #{Docyard::VERSION}"
14
+ end
15
+
16
+ desc "init", "Initialize a new docyard project"
17
+ def init
18
+ initializer = Docyard::Initializer.new
19
+ exit(1) unless initializer.run
20
+ end
21
+
22
+ desc "serve", "Start the development server"
23
+ method_option :port, type: :numeric, default: 4200, aliases: "-p", desc: "Port to run the server on"
24
+ method_option :host, type: :string, default: "localhost", aliases: "-h", desc: "Host to bind the server to"
25
+ def serve
26
+ require_relative "server"
27
+ server = Docyard::Server.new(
28
+ port: options[:port],
29
+ host: options[:host]
30
+ )
31
+ server.start
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "listen"
4
+
5
+ module Docyard
6
+ class FileWatcher
7
+ attr_reader :last_modified_time
8
+
9
+ def initialize(docs_path)
10
+ @docs_path = docs_path
11
+ @last_modified_time = Time.now
12
+ @listener = nil
13
+ end
14
+
15
+ def start
16
+ @listener = Listen.to(@docs_path, only: /\.md$/) do |modified, added, removed|
17
+ handle_changes(modified, added, removed)
18
+ end
19
+
20
+ @listener.start
21
+ end
22
+
23
+ def stop
24
+ @listener&.stop
25
+ rescue StandardError => e
26
+ puts "[Docyard] Error stopping file watcher: #{e.class} - #{e.message}"
27
+ end
28
+
29
+ def changed_since?(timestamp)
30
+ @last_modified_time > timestamp
31
+ end
32
+
33
+ private
34
+
35
+ def handle_changes(modified, added, removed)
36
+ return if modified.empty? && added.empty? && removed.empty?
37
+
38
+ @last_modified_time = Time.now
39
+ puts "[Docyard] Files changed, triggering reload..."
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+
5
+ module Docyard
6
+ class Initializer
7
+ DOCS_DIR = "docs"
8
+ TEMPLATE_DIR = File.join(__dir__, "templates", "markdown")
9
+
10
+ TEMPLATES = {
11
+ "index.md" => "index.md.erb",
12
+ "getting-started.md" => "getting-started.md.erb"
13
+ }.freeze
14
+
15
+ def initialize(path = ".")
16
+ @path = path
17
+ @docs_path = File.join(@path, DOCS_DIR)
18
+ end
19
+
20
+ def run
21
+ if already_initialized?
22
+ print_already_exists_error
23
+ return
24
+ end
25
+
26
+ create_structure
27
+ print_success
28
+ true
29
+ end
30
+
31
+ private
32
+
33
+ def already_initialized?
34
+ File.exist?(@docs_path)
35
+ end
36
+
37
+ def create_structure
38
+ FileUtils.mkdir_p(@docs_path)
39
+
40
+ TEMPLATES.each do |output_name, template_name|
41
+ copy_template(template_name, output_name)
42
+ end
43
+ end
44
+
45
+ def copy_template(template_name, output_name)
46
+ template_path = File.join(TEMPLATE_DIR, template_name)
47
+ output_path = File.join(@docs_path, output_name)
48
+
49
+ content = File.read(template_path)
50
+ File.write(output_path, content)
51
+ end
52
+
53
+ def print_already_exists_error
54
+ puts "Error: #{DOCS_DIR}/ folder already exists"
55
+ puts " Remove it first or run docyard in a different directory"
56
+ end
57
+
58
+ def print_success
59
+ puts "Docyard initialized successfully!"
60
+ puts ""
61
+ puts "Created:"
62
+ TEMPLATES.each_key { |file| puts " #{DOCS_DIR}/#{file}" }
63
+ puts ""
64
+ puts "Next steps:"
65
+ puts " 1. Edit your markdown files in #{DOCS_DIR}/"
66
+ puts " 2. Run 'docyard serve' to preview (not implemented yet)"
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "kramdown"
4
+ require "kramdown-parser-gfm"
5
+ require "yaml"
6
+
7
+ module Docyard
8
+ class Markdown
9
+ FRONTMATTER_REGEX = /\A---\s*\n(.*?\n)---\s*\n/m
10
+
11
+ attr_reader :raw
12
+
13
+ def initialize(raw)
14
+ @raw = raw.freeze
15
+ end
16
+
17
+ def frontmatter
18
+ @frontmatter ||= parse_frontmatter
19
+ end
20
+
21
+ def content
22
+ @content ||= extract_content
23
+ end
24
+
25
+ def html
26
+ @html ||= render_html
27
+ end
28
+
29
+ def title
30
+ frontmatter["title"]
31
+ end
32
+
33
+ def description
34
+ frontmatter["description"]
35
+ end
36
+
37
+ private
38
+
39
+ def parse_frontmatter
40
+ match = raw.match(FRONTMATTER_REGEX)
41
+ return {} unless match
42
+
43
+ YAML.safe_load(match[1])
44
+ rescue Psych::SyntaxError
45
+ {}
46
+ end
47
+
48
+ def extract_content
49
+ raw.sub(FRONTMATTER_REGEX, "").strip
50
+ end
51
+
52
+ def render_html
53
+ Kramdown::Document.new(
54
+ content,
55
+ input: "GFM",
56
+ hard_wrap: false,
57
+ syntax_highlighter: "rouge"
58
+ ).to_html
59
+ end
60
+ end
61
+ end