docco 0.1.0 → 0.1.1

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: 8f3ccd592996f2ebbe44ee3b9209210f2fac0772b6f8994483c6ad7d62d3db34
4
- data.tar.gz: e82c79a83e7e411a8dffce764b2a16d9749be276c38f21ff00261497aaf9b326
3
+ metadata.gz: cc8e122cfc342c995aafcd7f252fd4bf40c40e93954b8d53893cb4b1bc542b97
4
+ data.tar.gz: 7559ebddafbb6294e31650ce213a3526a32ed18bd065def3b396b713898255f9
5
5
  SHA512:
6
- metadata.gz: f9ca8eadfe55928385cfe667ade869202186e88a0ee3fc019d413bc73cb8fd0bf52cc789eac296554a797cd216bd0224799d9151a9e39c5c1064a37042d9e537
7
- data.tar.gz: 71081172e9925c33741b4f5be2d5329f5a8fec02ad27569166b982f6a2a87b0e21451cb93921f4995b6c563c48f09d46b35a7cbe1db6e0136530551cda2c1b86
6
+ metadata.gz: c01036732ff4b2b6db81ae86d1794c4200590fb222d6bdaa3a72a59681032ac8a402a0afb27ce25a27cd44b5cd96adaec5b389d6ba1c3071d85789eed53e6c79
7
+ data.tar.gz: eb4237fe63017abaccb95faf6f2edc003ee07a8cf7fa31693af18372e9d293316a7ffdb01e046ede9db1348176eebb793daeb4d5b60001445c676ca35eea8d62
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docco
4
+ class Builder
5
+ class SectionBuilder
6
+ attr_reader :root, :nodes, :info, :path, :to_path, :sections
7
+
8
+ def initialize(root:, node:, path:, children:, info:)
9
+ @root = root
10
+ @node = node
11
+ @path = path
12
+ @nodes = children
13
+ @sections = @nodes.filter(&:section?)
14
+ @info = info
15
+ @path = [*path, node.id]
16
+ @to_path = @path.join('/')
17
+ end
18
+
19
+ def id = @node.id
20
+ def level = @node.level
21
+ def title_html = @node.title_html
22
+ def title = @node.title
23
+ def to_html = @node.to_html
24
+ def section? = @node.section?
25
+
26
+ def build(*args)
27
+ case args
28
+ in [template]
29
+ root.link self, template, to_path
30
+ in [String => path, template]
31
+ root.link self, template, path
32
+ end
33
+ end
34
+ end
35
+
36
+ attr_reader :nodes, :sections, :info, :root, :pages, :path, :to_path
37
+
38
+ def initialize(nodes:, info:, root: self)
39
+ @to_path = ''
40
+ @path = [@to_path].freeze
41
+ @nodes = nodes.map do |n|
42
+ wrap_node(n, @path)
43
+ end
44
+ @sections = @nodes.filter(&:section?)
45
+ @info = info
46
+ @root = root
47
+ @pages = {}
48
+ end
49
+
50
+ def root = self
51
+ def to_html = @nodes.reduce(+'') { |str, n| str << n.to_html }
52
+
53
+ def link(node, template, path)
54
+ return path if @pages.key?(path)
55
+
56
+ @pages[path] = true
57
+ content = template.(node)
58
+ @pages[path] = content
59
+ path
60
+ end
61
+
62
+ def visit(theme)
63
+ link self, theme, to_path
64
+ end
65
+
66
+ def build(*args)
67
+ case args
68
+ in [template]
69
+ link self, template, to_path
70
+ in [String => path, template]
71
+ link self, template, path
72
+ end
73
+ end
74
+
75
+ private
76
+
77
+ # def initialize(root:, node:, parent:, children:, info:)
78
+ def wrap_node(node, parent_path)
79
+ return node unless node.section?
80
+
81
+ children = node.nodes.map { |n| wrap_node(n, [*parent_path, node.id]) }
82
+ SectionBuilder.new(root: self, node:, path: parent_path, children:, info:)
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docco
4
+ class Parser
5
+ class ContentNode
6
+ def initialize(converter, node)
7
+ @converter = converter
8
+ @node = node
9
+ end
10
+
11
+ def inspect = %(<#{self.class}:#{@node.type} [#{@node.children}]>)
12
+ def section? = false
13
+
14
+ def to_html
15
+ @to_html ||= @converter.convert(@node, 0)
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docco
4
+ class Parser
5
+ class Root
6
+ attr_reader :nodes, :level
7
+
8
+ def initialize(converter)
9
+ @converter = converter
10
+ @nodes = []
11
+ @level = 0
12
+ end
13
+
14
+ def inspect = %(<#{self.class} [#{@nodes.size} nodes]>)
15
+ def section? = false
16
+
17
+ def <<(section)
18
+ @nodes << section
19
+ end
20
+
21
+ def add_content(node)
22
+ @nodes << ContentNode.new(@converter, node)
23
+ end
24
+
25
+ def to_html
26
+ @to_html ||= @nodes.reduce(+'') do |str, node|
27
+ str << node.to_html << "\n"
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docco
4
+ class Parser
5
+ class Section
6
+ HEADING_EXP = /<\s*h([1-6])\b[^>]*>(.*?)<\/\s*h\1\s*>/im
7
+
8
+ attr_reader :id, :options, :nodes
9
+
10
+ def initialize(converter:, node:)
11
+ @converter = converter
12
+ @node = node
13
+ @id = node.attr['id']
14
+ @options = node.options
15
+ @nodes = []
16
+ end
17
+
18
+ def inspect = %(<#{self.class}:H#{level}##{id} [#{nodes.size} nodes]>)
19
+ def section? = true
20
+
21
+ def level = @options[:level]
22
+
23
+ def <<(section)
24
+ @nodes << section
25
+ end
26
+
27
+ def add_content(node)
28
+ @nodes << ContentNode.new(@converter, node)
29
+ end
30
+
31
+ def title_html
32
+ @to_html ||= @converter.convert(@node, 0)
33
+ end
34
+
35
+ def title
36
+ @title ||= title_html.match(HEADING_EXP)[2]
37
+ end
38
+
39
+ def to_html
40
+ @nodes.reduce(title_html) do |str, node|
41
+ str << "\n" << node.to_html
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'kramdown'
4
+
5
+ module Docco
6
+ class Parser
7
+ def initialize(text, input: 'GFM')
8
+ @text = text
9
+ @input = input
10
+ end
11
+
12
+ def structure
13
+ @structure ||= build
14
+ end
15
+
16
+ private
17
+ # cc = Kramdown::Converter::Html.send(:new, doc.root, doc.options)
18
+ # cc.convert doc.children[0], 1 # <= 1 is indent and it's needed
19
+ def build
20
+ doc = Kramdown::Document.new(@text, input: @input, auto_ids: true)
21
+ converter = Kramdown::Converter::Html.send(:new, doc.root, doc.options)
22
+ root = Root.new(converter)
23
+ last_section = root
24
+ levels = Hash.new { |h, k| h[k] = [] }
25
+ levels[last_section.level] << last_section # root
26
+
27
+ doc.root.children.each do |child|
28
+ if child.type == :header
29
+ section = Section.new(converter:, node: child)
30
+ levels[section.level] << section
31
+ if (parent = levels[section.level - 1].last)
32
+ parent << section
33
+ end
34
+ last_section = section
35
+ else # not a section. Content belonging to last section, or directly to root
36
+ last_section.add_content child
37
+ end
38
+ end
39
+
40
+ root
41
+ end
42
+ end
43
+ end
44
+
45
+ require 'docco/parser/section'
46
+ require 'docco/parser/root'
47
+ require 'docco/parser/content_node'
data/lib/docco/tasks.rake CHANGED
@@ -3,12 +3,6 @@
3
3
  require 'docco'
4
4
 
5
5
  namespace :docco do
6
- desc 'Copy default styles.css to host library docs'
7
- task :css, [:output_dir] do |t, args|
8
- output_dir = args[:output_dir] || 'docs'
9
- Docco::CopyStyles.(output_dir)
10
- end
11
-
12
6
  desc 'Generate a Github Action into .github/workflows'
13
7
  task :gh do
14
8
  Docco::CopyGHAction.()
@@ -0,0 +1,134 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'erb'
4
+
5
+ module Docco
6
+ # Theme is a templating system that uses ERB templates with named slots.
7
+ # It allows you to define a base template and then create specialized
8
+ # versions by filling in slots with different content.
9
+ class Theme
10
+ class Static
11
+ def initialize(path, content)
12
+ @path = path
13
+ @content = content.respond_to?(:read) ? content : StringIO.new(content)
14
+ end
15
+ end
16
+
17
+ class Slots
18
+ def initialize
19
+ @slots = {}
20
+ end
21
+
22
+ def slot(name, str)
23
+ @slots[name] = ERB.new(str)
24
+ end
25
+
26
+ def to_h = @slots
27
+ end
28
+
29
+ # Creates a new Theme from an ERB template string.
30
+ #
31
+ # @param str [String, #read] ERB template string that defines the layout
32
+ # @return [Theme] a new Theme instance with the compiled template
33
+ #
34
+ # @example Create a basic layout theme
35
+ # Layout = Docco::Theme.define <<~HTML
36
+ # <html>
37
+ # <head>
38
+ # <title><%= slots[:doc_title] || 'Home' %></title>
39
+ # </head>
40
+ # <body>
41
+ # <%= slots[:main] %>
42
+ # </body>
43
+ # </html>
44
+ # HTML
45
+ #
46
+ # Template can also be a #read() => String interface
47
+ # for example a File or Pathname to read templates from disk
48
+ # @example
49
+ # Page = Docco::Theme.define(Pathname.new('./theme/page.erb'))
50
+ def self.define(str)
51
+ str = str.read if str.respond_to?(:read)
52
+ tmpl = ERB.new(str)
53
+ new(tmpl)
54
+ end
55
+
56
+ def self.call(node)
57
+ raise NotImplementedError, "define #{self}.call(node) to delegate .call(node) to the right root template (usually the homepage)"
58
+ end
59
+
60
+ # Initializes a new Theme instance.
61
+ #
62
+ # @param tpl [ERB] compiled ERB template
63
+ # @param slots [Hash<Symbol, ERB>] hash of named slots with their ERB templates
64
+ # @return [Theme] a new Theme instance
65
+ #
66
+ # @example Create a theme with template and slots
67
+ # tpl = ERB.new("<div><%= slots[:content] %></div>")
68
+ # slots = { content: ERB.new("<p>Hello</p>") }
69
+ # theme = Docco::Theme.new(tpl, slots: slots)
70
+ def initialize(tpl, slots: {})
71
+ @tpl = tpl
72
+ @slots = slots
73
+ end
74
+
75
+ # Creates a new theme by defining slots for the template.
76
+ # Can accept either a string (which becomes the :main slot) or a block
77
+ # that yields a Slots object for defining multiple slots.
78
+ #
79
+ # @param str [String, nil] optional string to use as the :main slot
80
+ # @yield [Slots] yields a Slots object for defining named slots
81
+ # @return [Theme] a new Theme instance with the defined slots
82
+ #
83
+ # @example Define a theme with a string for the main slot
84
+ # HomeTemplate = Layout.define <<~HTML
85
+ # <h1>Home page</h1>
86
+ # <% page.sections.each do |s| %>
87
+ # <%= s.title_html %>
88
+ # <% end %>
89
+ # HTML
90
+ #
91
+ # @example Define a theme with multiple slots using a block
92
+ # PageTemplate = Layout.define do |tpl|
93
+ # tpl.slot :doc_title, '<%= page.title %>'
94
+ # tpl.slot :main, <<~HTML
95
+ # <h1><%= page.title %></h1>
96
+ # <% page.nodes.each do |n| %>
97
+ # <%= n.to_html %>
98
+ # <% end %>
99
+ # HTML
100
+ # end
101
+ def define(str = nil, &)
102
+ slots = Slots.new
103
+ if str
104
+ slots.slot(:main, str)
105
+ elsif block_given?
106
+ yield slots
107
+ end
108
+ self.class.new(@tpl, slots: slots.to_h)
109
+ end
110
+
111
+ Context = Data.define(:node, :slots) do
112
+ def page = node
113
+ def get_binding = binding
114
+ end
115
+
116
+ # Renders the theme with the given node by evaluating all slot templates
117
+ # and then the main template.
118
+ #
119
+ # @param node [Object] the node object to render (available as 'page' in templates)
120
+ # @return [String] the rendered HTML output
121
+ #
122
+ # @example Render a theme with a node
123
+ # output = PageTemplate.call(node)
124
+ # # => "<html><head>...</head><body>...</body></html>"
125
+ def call(node)
126
+ ctx = Context.new(node:, slots: {})
127
+ slots = @slots.transform_values do |tpl|
128
+ tpl.result(ctx.get_binding)
129
+ end
130
+ ctx = Context.new(node:, slots:)
131
+ @tpl.result(ctx.get_binding)
132
+ end
133
+ end
134
+ end
@@ -159,6 +159,16 @@ body {
159
159
  margin: 0;
160
160
  }
161
161
 
162
+ .nav-menu ul {
163
+ list-style: none;
164
+ padding-left: 0;
165
+ margin: 0;
166
+ }
167
+
168
+ .nav-menu ul li {
169
+ margin: 0;
170
+ }
171
+
162
172
  .nav-menu a {
163
173
  display: block;
164
174
  padding: 0.625rem 1.5rem;
@@ -241,6 +251,14 @@ body {
241
251
  padding-left: calc(3.5rem - 3px);
242
252
  }
243
253
 
254
+ /* Code in navigation */
255
+ .nav-menu a code {
256
+ background-color: transparent;
257
+ border: none;
258
+ padding: 0;
259
+ font-size: inherit;
260
+ }
261
+
244
262
  /* Main Content */
245
263
  .content {
246
264
  flex: 1;
@@ -275,6 +293,10 @@ body {
275
293
  .section {
276
294
  margin-bottom: 4rem;
277
295
  }
296
+ .section img {
297
+ max-width: 100%;
298
+ height: auto;
299
+ }
278
300
 
279
301
  .section h2 {
280
302
  font-size: 2rem;
@@ -401,6 +423,13 @@ pre code::-webkit-scrollbar-thumb {
401
423
  border-radius: 4px;
402
424
  }
403
425
 
426
+ pre.mermaid {
427
+ background: transparent;
428
+ box-shadow: none;
429
+ padding: 0;
430
+ text-align: center;
431
+ }
432
+
404
433
  /* Images */
405
434
  .image-container {
406
435
  margin: 2rem 0;
@@ -0,0 +1,182 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'docco/theme'
4
+
5
+ module Docco
6
+ module Themes
7
+ class Default < Theme
8
+ Styles = define(Pathname.new(File.join(__dir__, 'default.css')))
9
+
10
+ Layout = define <<~HTML
11
+ <!DOCTYPE html>
12
+ <html lang="en">
13
+ <head>
14
+ <meta charset="UTF-8">
15
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
16
+ <title><%= page.root.info.name %> - <%= page.root.info.description %></title>
17
+ <link rel="stylesheet" href="<%= page.build('styles.css', Docco::Themes::Default::Styles) %>">
18
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/atom-one-dark.min.css">
19
+ </head>
20
+ <body>
21
+ <nav class="top-menu">
22
+ <div class="top-menu-content">
23
+ <div class="top-menu-brand">
24
+ <span class="brand-name"><%= page.root.info.name %></span>
25
+ <span class="brand-tagline"><%= page.root.info.description %></span>
26
+ </div>
27
+ <a href="<%= page.root.info.repo_url %>" target="_blank" class="github-link" aria-label="View on GitHub">
28
+ <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
29
+ <path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
30
+ </svg>
31
+ <span>GitHub</span>
32
+ </a>
33
+ </div>
34
+ </nav>
35
+ <div class="container">
36
+ <nav class="sidebar">
37
+ <div class="logo">
38
+ <h2><%= page.root.info.name %></h2>
39
+ <p class="tagline"><%= page.root.info.description %></p>
40
+ </div>
41
+ <%= Docco::Themes::Default::Menu.(page) %>
42
+ </nav>
43
+
44
+ <main class="content">
45
+ <header class="page-header">
46
+ <h1><%= page.root.info.name %></h1>
47
+ <p class="subtitle"><%= page.root.info.summary %></p>
48
+ </header>
49
+
50
+ <%= slots[:main] %>
51
+ </main>
52
+ </div>
53
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
54
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/ruby.min.js"></script>
55
+ <script src="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js"></script>
56
+ <script>
57
+ document.querySelectorAll('pre > code.language-mermaid').forEach((code) => {
58
+ const pre = code.parentElement;
59
+ const container = document.createElement('pre');
60
+ container.className = 'mermaid';
61
+ container.textContent = code.textContent;
62
+ pre.replaceWith(container);
63
+ });
64
+ mermaid.initialize({ startOnLoad: false, theme: 'default' });
65
+ mermaid.run();
66
+ hljs.highlightAll();
67
+ </script>
68
+ <script>
69
+ // Active section highlighting
70
+ const observerOptions = {
71
+ root: null,
72
+ rootMargin: '-20% 0px -60% 0px',
73
+ threshold: 0
74
+ };
75
+
76
+ const sections = document.querySelectorAll('section, article[id]');
77
+ const navLinks = document.querySelectorAll('.nav-menu a');
78
+
79
+ // Create a map of href to link elements
80
+ const linkMap = new Map();
81
+ navLinks.forEach(link => {
82
+ const href = link.getAttribute('href');
83
+ if (href && href.startsWith('#')) {
84
+ linkMap.set(href, link);
85
+ }
86
+ });
87
+
88
+ const observer = new IntersectionObserver((entries) => {
89
+ entries.forEach(entry => {
90
+ if (entry.isIntersecting) {
91
+ const id = entry.target.getAttribute('id');
92
+ const activeLink = linkMap.get(`#${id}`);
93
+
94
+ if (activeLink) {
95
+ // Remove active class from all links
96
+ navLinks.forEach(link => link.classList.remove('active'));
97
+ // Add active class to current link
98
+ activeLink.classList.add('active');
99
+
100
+ // Update URL hash without scrolling
101
+ if (history.replaceState) {
102
+ history.replaceState(null, null, `#${id}`);
103
+ } else {
104
+ window.location.hash = id;
105
+ }
106
+ }
107
+ }
108
+ });
109
+ }, observerOptions);
110
+
111
+ // Observe all sections
112
+ sections.forEach(section => {
113
+ if (section.id) {
114
+ observer.observe(section);
115
+ }
116
+ });
117
+ </script>
118
+ </body>
119
+ </html>
120
+ HTML
121
+
122
+ Menu = Theme.define <<~HTML
123
+ <ul class="nav-menu">
124
+ <% page.sections.each do |section| %>
125
+ <% section.sections.each do |section| %>
126
+ <li>
127
+ <a href="#<%= section.id %>"><%= section.title %></a>
128
+ <% if section.sections.any? %>
129
+ <ul>
130
+ <% section.sections.each do |section| %>
131
+ <li class="nav-submenu">
132
+ <a href="#<%= section.id %>"><%= section.title %></a>
133
+ </li>
134
+ <% end %>
135
+ </ul>
136
+ <% end %>
137
+ </li>
138
+ <% end %>
139
+ <% end %>
140
+ </ul>
141
+ HTML
142
+
143
+ Section = Theme.define <<~HTML
144
+ <section id="<%= page.id %>" class="section">
145
+ <h2><%= page.title %></h2>
146
+ <% page.nodes.each do |node| %>
147
+ <% if node.section? %>
148
+ <article id="<%= node.id %>" class="subsection">
149
+ <h3><%= node.title %></h3>
150
+ <% node.nodes.each do |n| %>
151
+ <%= n.to_html %>
152
+ <% end %>
153
+ </article>
154
+ <% else %>
155
+ <%= node.to_html %>
156
+ <% end %>
157
+ <% end %>
158
+ </section>
159
+ HTML
160
+
161
+ HomePageTemplate = Layout.define <<~HTML
162
+ <% page.nodes.each do |node| %>
163
+ <% if node.section? %>
164
+ <% node.nodes.each do |node| %>
165
+ <% if node.section? %>
166
+ <%= Docco::Themes::Default::Section.(node) %>
167
+ <% else %>
168
+ <%= node.to_html %>
169
+ <% end %>
170
+ <% end %>
171
+ <% else %>
172
+ <%= node.to_html %>
173
+ <% end %>
174
+ <% end %>
175
+ HTML
176
+
177
+ def self.call(node)
178
+ HomePageTemplate.call(node)
179
+ end
180
+ end
181
+ end
182
+ end
data/lib/docco/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Docco
4
- VERSION = "0.1.0"
4
+ VERSION = "0.1.1"
5
5
  end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pathname'
4
+
5
+ module Docco
6
+ # Writes documents to the file system.
7
+ #
8
+ # The Writer class handles writing page content to disk with proper directory creation,
9
+ # path handling, and optional overwrite protection. It transforms logical page paths
10
+ # into actual file system paths, appending 'index.html' to directory paths as needed.
11
+ #
12
+ # @example Writing pages to output directory
13
+ # pages = {
14
+ # '' => '<html>Home</html>',
15
+ # '/docs' => '<html>Docs</html>',
16
+ # '/styles.css' => 'body { color: red; }'
17
+ # }
18
+ # writer = Docco::Writer.new(pages, output_dir: 'output', overwrite: false)
19
+ # report = writer.write
20
+ #
21
+ # The above example writes the following files:
22
+ # output/index.html
23
+ # output/docs/index.html
24
+ # output/styles.css
25
+ #
26
+ class Writer
27
+ # Initializes a new Writer instance with pages and output configuration.
28
+ #
29
+ # Transforms page paths by:
30
+ # - Converting relative paths to absolute paths within output_dir
31
+ # - Appending 'index.html' to paths without file extensions (directory paths)
32
+ # - Creating Pathname objects for each path
33
+ #
34
+ # @param pages [Hash<String, String>] Hash of page paths to content strings.
35
+ # Keys are logical page paths (e.g., '', '/docs', '/assets/style.css').
36
+ # Values are the content to write to those files.
37
+ #
38
+ # @param output_dir [String] The root directory where pages will be written.
39
+ # All page paths will be relative to this directory.
40
+ #
41
+ # @param overwrite [Boolean] Whether to overwrite existing files.
42
+ # If false (default), existing files will not be modified.
43
+ # If true, existing files will be overwritten with new content.
44
+ #
45
+ # @example
46
+ # writer = Docco::Writer.new(
47
+ # { '' => 'Home', '/docs' => 'Documentation' },
48
+ # output_dir: 'output',
49
+ # overwrite: false
50
+ # )
51
+ def initialize(pages, output_dir:, overwrite: false)
52
+ @pages = pages.transform_keys do |path|
53
+ path = Pathname.new(File.join(output_dir, path))
54
+ path += 'index.html' if path.extname.empty?
55
+ path
56
+ end
57
+
58
+ @overwrite = overwrite
59
+ end
60
+
61
+ def write
62
+ @pages.each.with_object({}) do |(path, content), memo|
63
+ memo[path.to_s] = write_page(path, content)
64
+ end
65
+ end
66
+
67
+ private def write_page(path, content)
68
+ return false if !@overwrite && path.exist?
69
+
70
+ path.dirname.mkpath
71
+ path.write(content)
72
+ end
73
+ end
74
+ end
data/lib/docco.rb CHANGED
@@ -1,8 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'kramdown'
4
3
  require 'fileutils'
5
4
  require_relative "docco/version"
5
+ require_relative "docco/parser"
6
+ require_relative "docco/builder"
7
+ require_relative "docco/writer"
8
+ require_relative "docco/themes/default"
6
9
 
7
10
  module Docco
8
11
  STYLES = 'styles.css'
@@ -17,46 +20,39 @@ module Docco
17
20
  puts "Github action copied to #{destination}"
18
21
  end
19
22
 
20
- CopyStyles = proc do |output_dir|
21
- FileUtils.mkdir_p(output_dir)
22
- source = File.join(__dir__, 'docco', STYLES)
23
- destination = File.join(output_dir, STYLES)
24
- FileUtils.cp(source, destination)
25
- puts "Docco styles copied to #{destination}"
23
+ def self.parse(text)
24
+ parser = Parser.new(text)
25
+ parser.structure
26
+ end
27
+
28
+ def self.write(pages, output_dir:, overwrite: false)
29
+ writer = Writer.new(pages, output_dir:, overwrite:)
30
+ writer.write
26
31
  end
27
32
 
33
+ Info = Data.define(:name, :summary, :description, :repo_url)
34
+
28
35
  class DocsBuilder
29
36
  def initialize(readme_path:, output_dir:, gemspec_path: nil)
30
37
  @readme_path = readme_path
31
38
  @output_dir = output_dir
32
- @sections = []
33
- @gemspec_path = gemspec_path || find_gemspec
34
- load_gemspec_info
39
+ @info = load_gemspec_info(gemspec_path || find_gemspec)
35
40
  end
36
41
 
37
- def build
38
- css = File.join(@output_dir, STYLES)
39
- if !File.exist?(css)
40
- CopyStyles.(@output_dir)
41
- end
42
-
42
+ def build(overwrite: false)
43
43
  puts "Reading #{@readme_path}..."
44
44
  markdown = File.read(@readme_path)
45
45
 
46
46
  puts "Parsing markdown..."
47
- doc = Kramdown::Document.new(markdown, input: 'GFM', auto_ids: true)
48
-
49
- puts "Extracting structure..."
50
- extract_structure(doc.root)
47
+ root = Docco.parse(markdown)
51
48
 
52
- puts "Generating HTML..."
53
- html = generate_html(doc)
49
+ builder = Docco::Builder.new(nodes: root.nodes, info: @info)
54
50
 
55
- puts "Writing to #{@output_dir}/index.html..."
56
- FileUtils.mkdir_p(@output_dir)
57
- File.write(File.join(@output_dir, 'index.html'), html)
58
-
59
- puts "Done! Documentation built successfully."
51
+ builder.visit(Docco::Themes::Default)
52
+ report = Docco.write(builder.pages, output_dir: @output_dir, overwrite:)
53
+ report.each do |path, written|
54
+ puts "Wrote file #{path}" if written
55
+ end
60
56
  end
61
57
 
62
58
  private
@@ -74,20 +70,22 @@ module Docco
74
70
  end
75
71
  end
76
72
 
77
- def load_gemspec_info
78
- if @gemspec_path && File.exist?(@gemspec_path)
79
- spec = Gem::Specification.load(@gemspec_path)
80
- @gem_name = spec.name
81
- @gem_summary = spec.summary
82
- @gem_description = spec.description
83
- # Prefer source_code_uri from metadata, fall back to homepage
84
- @github_url = spec.metadata['source_code_uri'] || spec.homepage
73
+ def load_gemspec_info(gemspec_path)
74
+ if gemspec_path && File.exist?(gemspec_path)
75
+ spec = Gem::Specification.load(gemspec_path)
76
+ Info.new(
77
+ name: spec.name,
78
+ summary: spec.summary,
79
+ description: spec.description,
80
+ repo_url: spec.metadata['source_code_uri'] || spec.homepage
81
+ )
85
82
  else
86
- # Fallback values if no gemspec found
87
- @gem_name = 'Documentation'
88
- @gem_summary = 'Project Documentation'
89
- @gem_description = 'Project Documentation'
90
- @github_url = ''
83
+ Info.new(
84
+ name: 'Documentation',
85
+ summary: 'Project docs',
86
+ description: 'Project docs',
87
+ repo_url: nil
88
+ )
91
89
  end
92
90
  end
93
91
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: docco
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ismael Celis
@@ -45,15 +45,24 @@ extensions: []
45
45
  extra_rdoc_files: []
46
46
  files:
47
47
  - lib/docco.rb
48
+ - lib/docco/builder.rb
48
49
  - lib/docco/deploy-docs.yml
49
- - lib/docco/styles.css
50
+ - lib/docco/parser.rb
51
+ - lib/docco/parser/content_node.rb
52
+ - lib/docco/parser/root.rb
53
+ - lib/docco/parser/section.rb
50
54
  - lib/docco/tasks.rake
51
55
  - lib/docco/tasks.rb
56
+ - lib/docco/theme.rb
57
+ - lib/docco/themes/default.css
58
+ - lib/docco/themes/default.rb
52
59
  - lib/docco/version.rb
53
- homepage: https://github.com/ismasan/docco
60
+ - lib/docco/writer.rb
61
+ homepage: https://ismasan.github.io/docco
54
62
  licenses: []
55
63
  metadata:
56
- homepage_uri: https://github.com/ismasan/docco
64
+ homepage_uri: https://ismasan.github.io/docco
65
+ source_code_uri: https://github.com/ismasan/docco
57
66
  post_install_message: |2+
58
67
 
59
68
  +----------------------------+
@@ -64,7 +73,7 @@ post_install_message: |2+
64
73
 
65
74
  Now you can run `bundle exec rake docco:docs` to generate HTML docs from your README.md and .gemspec
66
75
 
67
- You can also run `bundle exec rake docco:gh` to add a Github action to generate docs to Github Pages on deply.
76
+ You can also run `bundle exec rake docco:gh` to add a Github action to generate docs to Github Pages on deploy.
68
77
  +-----------------------------+
69
78
 
70
79
  rdoc_options: []
@@ -81,7 +90,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
81
90
  - !ruby/object:Gem::Version
82
91
  version: '0'
83
92
  requirements: []
84
- rubygems_version: 3.6.9
93
+ rubygems_version: 4.0.8
85
94
  specification_version: 4
86
95
  summary: Builds static HTML documentation from a Ruby gem's README
87
96
  test_files: []