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 +4 -4
- data/lib/docco/builder.rb +85 -0
- data/lib/docco/parser/content_node.rb +19 -0
- data/lib/docco/parser/root.rb +32 -0
- data/lib/docco/parser/section.rb +46 -0
- data/lib/docco/parser.rb +47 -0
- data/lib/docco/tasks.rake +0 -6
- data/lib/docco/theme.rb +134 -0
- data/lib/docco/{styles.css → themes/default.css} +29 -0
- data/lib/docco/themes/default.rb +182 -0
- data/lib/docco/version.rb +1 -1
- data/lib/docco/writer.rb +74 -0
- data/lib/docco.rb +38 -40
- metadata +15 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cc8e122cfc342c995aafcd7f252fd4bf40c40e93954b8d53893cb4b1bc542b97
|
|
4
|
+
data.tar.gz: 7559ebddafbb6294e31650ce213a3526a32ed18bd065def3b396b713898255f9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
data/lib/docco/parser.rb
ADDED
|
@@ -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.()
|
data/lib/docco/theme.rb
ADDED
|
@@ -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
data/lib/docco/writer.rb
ADDED
|
@@ -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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
@
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
puts "Extracting structure..."
|
|
50
|
-
extract_structure(doc.root)
|
|
47
|
+
root = Docco.parse(markdown)
|
|
51
48
|
|
|
52
|
-
|
|
53
|
-
html = generate_html(doc)
|
|
49
|
+
builder = Docco::Builder.new(nodes: root.nodes, info: @info)
|
|
54
50
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
|
79
|
-
spec = Gem::Specification.load(
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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.
|
|
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/
|
|
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
|
-
|
|
60
|
+
- lib/docco/writer.rb
|
|
61
|
+
homepage: https://ismasan.github.io/docco
|
|
54
62
|
licenses: []
|
|
55
63
|
metadata:
|
|
56
|
-
homepage_uri: https://github.
|
|
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
|
|
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:
|
|
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: []
|