├── .gitignore ├── Gemfile ├── HISTORY.md ├── README.md ├── Rakefile ├── bin └── reacco ├── data ├── index.erb └── style.css ├── lib ├── reacco.rb └── reacco │ ├── extractor.rb │ ├── extractor │ └── block.rb │ ├── filters │ ├── brief.rb │ ├── headingid.rb │ ├── hgroup.rb │ ├── literate.rb │ ├── prelang.rb │ ├── sections.rb │ └── toc.rb │ ├── generator.rb │ ├── readme.rb │ └── version.rb ├── reacco.gemspec └── test ├── blocks_test.rb └── test_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | /doc/ 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source :rubygems 2 | gemspec 3 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | v0.0.3 - Sep 19, 2011 2 | --------------------- 3 | 4 | ### Added: 5 | * Support Google Analytics. 6 | 7 | ### Fixed: 8 | * CSS issues (float drops, the sectioning being crappy). 9 | 10 | v0.0.2 - Sep 19, 2011 11 | --------------------- 12 | 13 | Initial semi-public version. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [Reacco](http://ricostacruz.com/reacco) 2 | #### Readme documentation prettifier 3 | 4 | Reacco is a dead simple documentation generator that lets you document your 5 | project using Markdown. 6 | 7 | ## Usage 8 | 9 | #### Installation 10 | Install Reacco first. It is a Ruby gem. 11 | 12 | $ gem install reacco 13 | 14 | #### Generation 15 | To generate documentation, type `reacco`. This takes your `README.md` file and 16 | prettifies it. 17 | 18 | $ reacco 19 | 20 | #### Literate style blocks 21 | To make literate-style blocks (that is, code on the right, explanation on the 22 | left), use `reacco --literate`. 23 | 24 | $ reacco --literate 25 | 26 | ## Documenting API 27 | To extract documentation from your code files, add `--api `. This 28 | extracts comment blocks from files in that path. 29 | 30 | As Reacco only parses out Markdown from your comments, almost all languages that 31 | comments in `#` and `//` are supported. It does not care about code at all, just 32 | comments. 33 | 34 | $ reacco --literate --api lib/ 35 | 36 | #### Documenting classes 37 | You will need to add Markdown comment blocks to your code. The first line needs 38 | to be a Markdown heading in the form of `### `. 39 | 40 | Classes are often made to be H2's. 41 | 42 | ``` ruby 43 | # ## Reacco [class] 44 | # The main class. 45 | # 46 | # Class documentation goes here in Markdown form. 47 | # 48 | class Reacco 49 | ... 50 | end 51 | ``` 52 | 53 | #### Documenting class methods 54 | Class methods are often made as H3's. Sub-sections are often always H4's. 55 | 56 | ``` ruby 57 | # ## Reacco [class] 58 | # The main class. 59 | # 60 | class Reacco 61 | # ### version [class method] 62 | # Returns a string of the library's version. 63 | # 64 | # #### Example 65 | # This example returns the version. 66 | # 67 | # Reacco.version 68 | # #=> "0.0.1" 69 | # 70 | def self.version 71 | ... 72 | end 73 | end 74 | ``` 75 | 76 | #### Adding the placeholder 77 | To specify where the docs will be in the README, put a line with the text 78 | `[](#api_reference)`. This will tell Reacco where to "inject" your API 79 | documentation. 80 | 81 | # README.md: 82 | Please see http://you.github.com/project. [](#api_reference) 83 | 84 | # API reference 85 | 86 | For usage and API reference, please see http://ricostacruz.com/reacco. [](#api_reference) 87 | 88 | Warning 89 | ------- 90 | 91 | **Here be dragons!** this is mostly made for my own projects, so I may change 92 | things quite often (though I'd try to be mostly API-compatible with older 93 | versions). 94 | 95 | Acknowledgements 96 | ---------------- 97 | 98 | © 2011, Rico Sta. Cruz. Released under the [MIT 99 | License](http://www.opensource.org/licenses/mit-license.php). 100 | 101 | Reacco is authored and maintained by [Rico Sta. Cruz][rsc] with help from it's 102 | [contributors][c]. It is sponsored by my startup, [Sinefunc, Inc][sf]. 103 | 104 | * [My website](http://ricostacruz.com) (ricostacruz.com) 105 | * [Sinefunc, Inc.](http://sinefunc.com) (sinefunc.com) 106 | * [Github](http://github.com/rstacruz) (@rstacruz) 107 | * [Twitter](http://twitter.com/rstacruz) (@rstacruz) 108 | 109 | [rsc]: http://ricostacruz.com 110 | [c]: http://github.com/rstacruz/reacco/contributors 111 | [sf]: http://sinefunc.com 112 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | desc "Invokes the test suite in multiple RVM environments" 2 | task :'test!' do 3 | # Override this by adding RVM_TEST_ENVS=".." in .rvmrc 4 | envs = ENV['RVM_TEST_ENVS'] || '1.9.2@reacco,,1.8.7@reacco' 5 | puts "* Testing in the following RVM environments: #{envs.gsub(',', ', ')}" 6 | system "rvm #{envs} rake test" or abort 7 | end 8 | 9 | desc "Runs tests" 10 | task :test do 11 | Dir['test/*_test.rb'].each { |f| load f } 12 | end 13 | 14 | task :default => :test 15 | 16 | gh = "rstacruz/reacco" 17 | namespace :doc do 18 | # http://github.com/rstacruz/reacco 19 | desc "Builds the documentation into doc/" 20 | task :build do 21 | analytics = "--analytics #{ENV['ANALYTICS_ID']}" if ENV['ANALYTICS_ID'] 22 | system "reacco -a --github #{gh} #{analytics} --api lib" 23 | end 24 | 25 | # http://github.com/rstacruz/git-update-ghpages 26 | desc "Posts documentation to GitHub pages" 27 | task :deploy => :build do 28 | system "git update-ghpages #{gh} -i doc/" 29 | end 30 | end 31 | 32 | task :doc => :'doc:build' 33 | -------------------------------------------------------------------------------- /bin/reacco: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | $:.unshift File.expand_path('../../lib', __FILE__) 3 | require 'reacco' 4 | 5 | module Params 6 | def extract(what) i = index(what) and slice!(i, 2)[1] end; 7 | end 8 | ARGV.extend Params 9 | 10 | def tip(str) 11 | $stderr.write "#{str}\n" 12 | end 13 | 14 | if ARGV == ['-h'] || ARGV == ['--help'] 15 | tip "Usage: reacco [options]" 16 | tip "Generates nice HTML." 17 | tip "" 18 | tip "Options:" 19 | tip " --literate Move
 blocks to the right."
20 |   tip "      --toc              Show a table of contents."
21 |   tip ""
22 |   tip "Theming:"
23 |   tip "      --css FILE         Append the CSS from that file to the generated CSS."
24 |   tip "  -t, --template PATH    The template to use, if you don't want the default."
25 |   tip ""
26 |   tip "API extraction:"
27 |   tip "      --api PATH         Where to get stuff."
28 |   tip ""
29 |   tip "More:"
30 |   tip "      --analytics ID     Google Analytics ID."
31 |   tip "      --github REPO      Adds a 'Fork me on GitHub' badge. Repo should be in"
32 |   tip "                         'username/reponame' format."
33 |   tip "  -o, --output PATH      Sets the path to put HTML files in. Defaults to 'doc/'."
34 |   tip ""
35 |   exit
36 | 
37 | else
38 |   # Build options from arguments.
39 |   switches  = [:literate, :toc]
40 |   docpath   = ARGV.extract('-o') || ARGV.extract('--output') || 'doc'
41 |   template  = ARGV.extract('-t') || ARGV.extract('--template') || nil
42 |   analytics = ARGV.extract('--analytics') || nil
43 |   github    = ARGV.extract('--github') || nil
44 |   css       = ARGV.extract('--css') || nil
45 |   awesome   = ARGV.delete('-a')  || ARGV.extract('--awesome')
46 |   options   = Hash.new
47 | 
48 |   extract = Array.new
49 |   while (x = ARGV.extract('--api'))
50 |     extract << x
51 |   end
52 | 
53 |   switches.each do |opt|
54 |     options[opt] = true  if ARGV.delete("--#{opt}")
55 |   end
56 | 
57 |   switches.each { |opt| options[opt] = true }  if awesome
58 | 
59 |   options[:github]    = github     if github
60 |   options[:analytics] = analytics  if analytics
61 | 
62 |   if ARGV.any?
63 |     puts "Invalid usage. See `reacco --help` for help."
64 |     exit 60
65 |   end
66 | 
67 |   # Lets find the readme.
68 |   readme = Reacco::Readme.new(options)
69 |   unless readme.exists?
70 |     tip "Readme file not found."
71 |     exit 60
72 |   end
73 | 
74 |   # If we need to extract API, let's do it.
75 |   if extract.any?
76 |     blocks = extract.map { |path|
77 |       Reacco::Extractor.new(Dir["#{path}/**/*"]).blocks
78 |     }.flatten
79 | 
80 |     blocks.each { |blk| readme.inject_api_block blk.to_html }
81 |   end
82 | 
83 |   # Now let's generate it.
84 |   gen  = Reacco::Generator.new(readme, docpath, template, css)
85 |   unless gen.template?
86 |     tip "Invalid template."
87 |     tip "A template must at least have an index.xxx page."
88 |     exit 61
89 |   end
90 | 
91 |   gen.write! { |file| tip "* #{file}" }
92 | 
93 |   # AHHH YYEAAAHHH!
94 |   tip "Construction complete."
95 | end
96 | 
97 | 
98 | #tip Reacco.html(:hgroup => true, :literate => true, :brief => true)
99 | 


--------------------------------------------------------------------------------
/data/index.erb:
--------------------------------------------------------------------------------
 1 | 
 2 | 
 3 | 
 4 |     
 5 |     <%= title %>
 6 |     
 7 |     
 8 |     <% if analytics %>
 9 |       
20 |     <% end %>
21 | 
22 | 
23 |   <% if github %>
24 |     Fork me on GitHub
25 |   <% end %>
26 |   
<%= yield %>
27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /data/style.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | margin: 0; 3 | padding: 0; } 4 | 5 | body { 6 | background: #fcfcfa; 7 | font-family: 'pt sans', sans-serif; 8 | color: #444; 9 | font-size: 11pt; 10 | line-height: 1.5; } 11 | 12 | ::-moz-selection { background: #88a; color: #fff; text-shadow: none; } 13 | ::selection { background: #88a; color: #fff; text-shadow: none; } 14 | 15 | h1, h2, h3, h4, h5, h6, p, ol, ul, pre, ol>li { 16 | max-width: 500px; 17 | margin: 20px 0; } 18 | 19 | ol, ul { 20 | padding-left: 0; 21 | margin-left: 30px; } 22 | 23 | #all { 24 | min-width: 960px; 25 | box-sizing: border-box; 26 | -moz-box-sizing: border-box; 27 | -webkit-box-sizing: border-box; 28 | width: 100%; 29 | overflow-x: hidden; 30 | padding: 40px; } 31 | 32 | a, a:visited { 33 | text-decoration: none; 34 | border-bottom: solid 1px #ccf; 35 | color: #46b; } 36 | 37 | a:hover { 38 | background: #fafae0; } 39 | 40 | /* List */ 41 | ul li > p > strong:first-child { 42 | display: block; 43 | font-size: 1.1em; } 44 | 45 | /* Headings */ 46 | h1 { 47 | font-family: palatino, serif; } 48 | 49 | h1, h2, h3, h4, h5, h6 { 50 | text-shadow: 2px 2px 0 rgba(55, 55, 55, 0.1); 51 | color: #568; 52 | font-weight: normal; 53 | font-family: shanti, palatino, serif; } 54 | 55 | h3 { 56 | color: #77a; } 57 | 58 | h4, h5, h6 { 59 | color: #88a; } 60 | 61 | /* The "(class method)" thing */ 62 | h1 .type, h2 .type, h3 .type, h4 .type, h5 .type, h6 .type { 63 | font-size: 9pt; 64 | color: #aaa; 65 | text-transform: uppercase; 66 | margin-left: 10px; } 67 | 68 | h2 { 69 | max-width: none; 70 | padding-top: 30px; 71 | margin-top: 40px; } 72 | 73 | h3 { 74 | max-width: none; 75 | padding-top: 10px; 76 | margin-top: 40px; } 77 | 78 | h1 { font-size: 30pt; } 79 | h2 { font-size: 23pt; } 80 | h3 { font-size: 14pt; } 81 | h4 { font-size: 14pt; } 82 | h5 { font-size: 10pt; } 83 | h6 { font-size: 10pt; } 84 | 85 | h2+p { margin-top: 0; } 86 | h2+pre.right+p { margin-top: 0; } 87 | 88 | /* Code */ 89 | pre, code { 90 | font-family: Monaco, monospace; } 91 | 92 | code { 93 | text-shadow: 1px 1px 0 rgba(255, 255, 255, 0.99); 94 | font-family: 'shanti', sans-serif; 95 | color: #9090aa; } 96 | 97 | pre > code { 98 | color: #60606f; 99 | text-shadow: none; 100 | font-family: Monaco, monospace; 101 | background: 0; 102 | padding: 0; 103 | border: 0; } 104 | 105 | pre { 106 | font-size: 9pt; 107 | padding-top: 5px; 108 | background: transparent; } 109 | 110 | /* The header */ 111 | .header h1, .header h2, .header h3, 112 | .header h4, .header h5, .header h6 { 113 | text-shadow: none; 114 | line-height: 1.35; 115 | margin: 0; 116 | padding: 0; } 117 | 118 | .header h1 a, 119 | .header h1 { 120 | font-family: 'pt sans', sans-serif; 121 | font-weight: bold; 122 | color: #779; } 123 | 124 | .header h1 a { 125 | border-bottom: 0; } 126 | 127 | .header h2, .header h3, 128 | .header h4, .header h5, .header h6 { 129 | color: #aab; } /* color: #78b; */ 130 | 131 | .header h4 { 132 | font-size: 1.4em; 133 | font-family: palatino, serif; 134 | font-style: italic; 135 | font-weight: normal; } 136 | 137 | /* Literate */ 138 | body.literate h4, body.literate h5, body.literate h6, body.literate p, 139 | body.literate ol, body.literate ul, body.literate pre { 140 | max-width: 400px; } 141 | 142 | body.literate pre.right { 143 | clear: both; 144 | max-width: none; 145 | width: 490px; 146 | margin-left: 50px; 147 | float: right; } 148 | 149 | body.literate pre.right code { 150 | color: #78a; } 151 | 152 | body.literate h1 + pre.right, 153 | body.literate h2 + pre.right, 154 | body.literate h3 + pre.right { 155 | margin-top: 0; } 156 | 157 | h1, h2, h3 { 158 | clear: both; } 159 | 160 | .clear { 161 | clear: both; } 162 | 163 | br.post-pre { 164 | clear: both; } 165 | 166 | h4 { margin-bottom: 5px; } 167 | h4+p, 168 | h4+pre.right, 169 | h4+pre.right+p { margin-top: 0; } 170 | 171 | /* Section-wrap */ 172 | section { 173 | width: 960px; 174 | margin-top: 0; 175 | margin-left: 0; 176 | padding-left: 40px; 177 | margin-left: -40px; 178 | padding-right: 9000px; } 179 | 180 | section::after { 181 | content: ''; 182 | display: table; 183 | clear: both; } 184 | 185 | section.h2:nth-child(even) { 186 | box-shadow: 0 1px 10px rgba(0, 0, 0, 0.1); 187 | background: rgba(255, 255, 255, 0.8); } 188 | 189 | section:first-of-type { 190 | margin-top: 40px; } 191 | 192 | section.h1, section.h2, section.h3 { 193 | padding-top: 30px; 194 | padding-bottom: 40px; 195 | border-top: solid 1px #eee; } 196 | 197 | section.h1 { 198 | border-top: solid 10px #ddd; } 199 | 200 | section.h3 { 201 | padding-top: 20px; 202 | margin-top: 40px; 203 | padding-left: 30px; 204 | border-left: solid 10px #e7e7f4; 205 | border-top: dotted 1px #ddd; } 206 | 207 | section.h3:nth-child(odd) { 208 | border-left-color: #d0d0e8; } 209 | 210 | section.h3 + section.h3 { 211 | margin-top: -20px; } 212 | 213 | section.h1>:first-child, section.h2>:first-child, section.h3>:first-child { 214 | padding-top: 0; 215 | margin-top: 0; } 216 | 217 | /* API */ 218 | section.api h2, 219 | section.api h3 { 220 | margin-bottom: 0; } 221 | 222 | section.api h2 + p, 223 | section.api h2 + pre + p, 224 | section.api h3 + p, 225 | section.api h3 + pre + p { 226 | margin-top: 0; } 227 | 228 | h2 .args, 229 | h3 .args { 230 | margin-left: 3px; 231 | font-size: 0.9em; 232 | margin-right: 15px; 233 | color: #aab; } 234 | 235 | /* Pretty printing styles. Used with prettify.js. */ 236 | .str { color: #080; } 237 | .kwd { color: #008; } 238 | .com { color: #607077; background: rgba(0, 0, 0, 0.05); padding: 1px 3px; } 239 | .typ { color: #606; } 240 | .lit { color: #066; } 241 | .pun { color: #660; } 242 | .pln { color: #000; } 243 | .tag { color: #008; } 244 | .atn { color: #606; } 245 | .atv { color: #080; } 246 | .dec { color: #606; } 247 | 248 | /* Specify class=linenums on a pre to get line numbering */ 249 | ol.linenums { margin-top: 0; margin-bottom: 0 } /* IE indents via margin-left */ 250 | li.L0, li.L1, li.L2, li.L3, li.L5, li.L6, li.L7, li.L8 { list-style-type: none } 251 | /* Alternate shading for lines */ 252 | li.L1, li.L3, li.L5, li.L7, li.L9 { background: #eee } 253 | 254 | @media print { 255 | .str { color: #060; } 256 | .kwd { color: #006; font-weight: bold; } 257 | .com { color: #600; font-style: italic; } 258 | .typ { color: #404; font-weight: bold; } 259 | .lit { color: #044; } 260 | .pun { color: #440; } 261 | .pln { color: #000; } 262 | .tag { color: #006; font-weight: bold; } 263 | .atn { color: #404; } 264 | .atv { color: #060; } 265 | } 266 | 267 | /* TOC */ 268 | body.toc { 269 | margin-left: 200px; } 270 | 271 | aside#toc { 272 | font-size: 0.9em; 273 | line-height: 1.7; 274 | position: fixed; 275 | top: 0; 276 | left: 0; 277 | bottom: 0; 278 | padding: 20px; 279 | width: 180px; 280 | overflow-y: auto; 281 | box-shadow: inset -2px 0 2px rgba(0, 0, 0, 0.1); 282 | border-right: solid 1px #ccc; 283 | background: #f4f4fa; } 284 | 285 | aside#toc h1, 286 | aside#toc h2, 287 | aside#toc h3 { 288 | margin: 10px 0; 289 | padding: 0; 290 | font-size: 1.1em; } 291 | 292 | aside#toc h1 { 293 | color: #aaa; 294 | text-shadow: none; 295 | font-size: 1.3em; } 296 | 297 | aside#toc h2, 298 | aside#toc h3 { 299 | border-top: solid 1px #ddd; 300 | padding-top: 10px; } 301 | 302 | aside#toc h2 .type, 303 | aside#toc h3 .type { 304 | margin-top: 2px; 305 | margin-left: 0; } 306 | 307 | aside#toc ul, 308 | aside#toc li { 309 | margin: 0; 310 | padding: 0; 311 | list-style-type: none; } 312 | 313 | aside#toc h1 a, 314 | aside#toc h2 a, 315 | aside#toc h3 a { 316 | color: #333; } 317 | 318 | aside#toc a { 319 | height: 1.6em; 320 | overflow: hidden; 321 | line-height: 1.6em; 322 | position: relative; 323 | display: block; 324 | border-bottom: 0; 325 | color: #555; } 326 | 327 | aside#toc a span.args { 328 | display: none; } 329 | 330 | aside#toc a span.type { 331 | font-weight: normal; 332 | color: #999; 333 | text-transform: uppercase; 334 | font-size: 8pt; 335 | float: right; } 336 | 337 | aside#toc::-webkit-scrollbar { 338 | height: 15px; width: 15px; 339 | } 340 | 341 | aside#toc::-webkit-scrollbar-thumb { 342 | border-width: 7px 7px 7px 7px; 343 | -webkit-border-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYwIDYxLjEzNDc3NywgMjAxMC8wMi8xMi0xNzozMjowMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNSBNYWNpbnRvc2giIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6NDZEMERCMUZGRUI2MTFERkIwQ0RGMkFBNEE3NDlCOUMiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6NDZEMERCMjBGRUI2MTFERkIwQ0RGMkFBNEE3NDlCOUMiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo0NkQwREIxREZFQjYxMURGQjBDREYyQUE0QTc0OUI5QyIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo0NkQwREIxRUZFQjYxMURGQjBDREYyQUE0QTc0OUI5QyIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PpHo8kwAAACBSURBVHjaYvz//z8DJYCJgUJAsQEsyBxGRkYwBcS6QKwBxLJQqcdAfAOILwPxf2RvM6JwICbYA7EDDgsPAPFBZBPQvaCLRzMDVE4XXxhoEOFtDXwGyBJhgCxNo/ExEXoe4zPgBhEG3MBnwGVoVDHgicbLKGkHLR1QlpCGZmYCCDAAxuMoE4WhhWUAAAAASUVORK5CYII=) 7 7 7 7 round round; 344 | } 345 | -------------------------------------------------------------------------------- /lib/reacco.rb: -------------------------------------------------------------------------------- 1 | require 'nokogiri' 2 | require 'redcarpet' 3 | require 'tilt' 4 | require 'ostruct' 5 | require 'fileutils' 6 | 7 | # ## Reacco [module] 8 | # This is the main module. 9 | # 10 | module Reacco 11 | extend self 12 | 13 | # ### root [class method] 14 | # Returns the root path of the Reacco gem. 15 | # You may pass additional parameters. 16 | # 17 | # Reacco.root 18 | # #=> '/usr/local/ruby/gems/reacco-0.0.1' 19 | # 20 | def root(*a) 21 | File.join File.expand_path('../../', __FILE__), *a 22 | end 23 | 24 | # ### markdown [class method] 25 | # Returns the Redcarpet Markdown processor. This is an instance of 26 | # `Redcarpet` with all the right options plugged in. 27 | # 28 | # Reacco.markdown 29 | # #=> # 30 | # 31 | def markdown 32 | Redcarpet::Markdown.new(Redcarpet::Render::HTML, 33 | :fenced_code_blocks => true) 34 | end 35 | 36 | autoload :Readme, 'reacco/readme' 37 | autoload :Generator, 'reacco/generator' 38 | autoload :Extractor, 'reacco/extractor' 39 | 40 | module Filters 41 | autoload :Brief, 'reacco/filters/brief' 42 | autoload :Sections, 'reacco/filters/sections' 43 | autoload :Hgroup, 'reacco/filters/hgroup' 44 | autoload :Literate, 'reacco/filters/literate' 45 | autoload :PreLang, 'reacco/filters/prelang' 46 | autoload :HeadingID, 'reacco/filters/headingid' 47 | autoload :TOC, 'reacco/filters/toc' 48 | end 49 | end 50 | 51 | -------------------------------------------------------------------------------- /lib/reacco/extractor.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'ostruct' 4 | require 'fileutils' 5 | module Reacco 6 | # Reacco::Extractor [class] 7 | # Extracts comments from list of files. 8 | # 9 | # #### Instantiating 10 | # Call the constructor with a list of files. 11 | # 12 | # ex = Extractor.new(Dir['./**/*.rb']) 13 | # 14 | class Extractor 15 | autoload :Block, 'reacco/extractor/block' 16 | 17 | def initialize(files, root=nil, options={}) 18 | @files = files.sort 19 | @root = File.realpath(root || Dir.pwd) 20 | end 21 | 22 | # blocks [method] 23 | # Returns an array of `Block` instances. 24 | # 25 | # ex.blocks 26 | # ex.blocks.map { |b| puts "file: #{b.file}" } 27 | # 28 | def blocks 29 | @blocks ||= begin 30 | @files.map do |file| 31 | if File.file?(file) 32 | input = File.read(file) 33 | get_blocks(input, unroot(file)) 34 | end 35 | end.compact.flatten 36 | end 37 | end 38 | 39 | private 40 | def unroot(fn) 41 | (File.realpath(fn))[@root.size..-1] 42 | end 43 | 44 | # get_blocks(str, [filename]) [private method] 45 | # Returns the documentation blocks in a given `str`ing. 46 | # If `filename` is given, it will be set as the *source_file*. 47 | # 48 | def get_blocks(str, filename=nil) 49 | arr = get_comment_blocks(str) 50 | 51 | arr.map do |hash| 52 | block = hash[:block] 53 | 54 | # Ensure the first line matches. 55 | # This matches: 56 | # "### name [type]" 57 | # "## name(args) [type]" 58 | re = /^(\#{1,6}) (.*?) ?(\(.*?\))? ?(?:\[([a-z ]+)\])?$/ 59 | block.first =~ re or next 60 | 61 | blk = Extractor::Block.new \ 62 | :type => $4, 63 | :tag => "h#{$1.strip.size}", 64 | :title => $2, 65 | :args => $3, 66 | :source_line => hash[:line] + block.size + 1, 67 | :source_file => filename, 68 | :body => (block[1..-1].join("\n") + "\n") 69 | 70 | blk 71 | end.compact 72 | end 73 | 74 | # get_comment_blocks (private method) 75 | # Returns contiguous comment blocks. 76 | # 77 | # Returns an array of hashes that look like 78 | # `{ :block => [line1, line2...], :line => (line number) }` 79 | # 80 | def get_comment_blocks(str) 81 | chunks = Array.new 82 | i = 0 83 | 84 | str.split("\n").each_with_index { |s, line| 85 | if s =~ /^\s*(?:\/\/\/?|##?) ?(.*)$/ 86 | chunks[i] ||= { :block => Array.new, :line => line } 87 | chunks[i][:block] << $1 88 | else 89 | i += 1 if chunks[i] 90 | end 91 | } 92 | 93 | chunks.compact 94 | end 95 | end 96 | end 97 | -------------------------------------------------------------------------------- /lib/reacco/extractor/block.rb: -------------------------------------------------------------------------------- 1 | # Reacco::Extractor::Block [class] 2 | # An extractor block. 3 | # 4 | module Reacco 5 | class Extractor 6 | class Block 7 | # body [method] 8 | # Returns the body text as a raw string. 9 | attr_reader :body 10 | 11 | # title [method] 12 | # Returns the title of the block. 13 | attr_reader :title 14 | attr_reader :type 15 | attr_reader :parent 16 | attr_reader :args 17 | 18 | def initialize(options) 19 | @body = options[:body] 20 | @type = options[:type] && options[:type].downcase 21 | @args = options[:args] 22 | @title = options[:title] 23 | @parent = options[:parent] 24 | @tag = options[:tag] 25 | @source_line = options[:source_line] 26 | @source_path = options[:source_path] 27 | end 28 | 29 | # children [method] 30 | # Returns an array of child blocks. 31 | def children 32 | @children ||= Array.new 33 | end 34 | 35 | # << [method] 36 | # Adds a block to it's children. 37 | def <<(blk) 38 | children << blk 39 | end 40 | 41 | def raw_html 42 | Reacco.markdown.render(@body) 43 | end 44 | 45 | # Nokogiri node. 46 | def doc 47 | @doc ||= Nokogiri::HTML(raw_html) 48 | end 49 | 50 | def transform! 51 | # Create heading. 52 | name = @tag 53 | node = Nokogiri::XML::Node.new(name, doc) 54 | node['class'] = 'api' 55 | node.content = title 56 | 57 | # Add '(args)'. 58 | if args 59 | span = Nokogiri::XML::Node.new("span", doc) 60 | span['class'] = 'args' 61 | span.content = args 62 | node.add_child span 63 | end 64 | 65 | # Add '(class method)'. 66 | span = Nokogiri::XML::Node.new("span", doc) 67 | span['class'] = 'type' 68 | span.content = type 69 | node.add_child Nokogiri::XML::Text.new(' ', doc) 70 | node.add_child span 71 | 72 | # Add heading. 73 | doc.at_css('body>*:first-child').add_previous_sibling node 74 | doc 75 | end 76 | 77 | # to_html [method] 78 | # Returns the raw HTML to be included in the documentation. 79 | def to_html 80 | @to_html ||= transform!.at_css('body').inner_html 81 | end 82 | end 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /lib/reacco/filters/brief.rb: -------------------------------------------------------------------------------- 1 | module Reacco 2 | module Filters 3 | module Brief 4 | # Makes the first

a brief. 5 | def brief_first_p(html) 6 | p = html.at_css('body>p') 7 | p['class'] = "#{p['class']} brief" if p 8 | html 9 | end 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /lib/reacco/filters/headingid.rb: -------------------------------------------------------------------------------- 1 | module Reacco 2 | module Filters 3 | module HeadingID 4 | def heading_id(html) 5 | html.css('h1, h2, h3').each do |h| 6 | h['id'] = slugify(h.content) 7 | end 8 | 9 | html 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /lib/reacco/filters/hgroup.rb: -------------------------------------------------------------------------------- 1 | module Reacco 2 | module Filters 3 | module Hgroup 4 | # Wraps the first headings into an

. 5 | def wrap_hgroup(html) 6 | nodes = Array.new 7 | html.css('body>*').each { |node| 8 | # Consume all headings. 9 | if %w(h1 h2 h3 h4 h5 h6).include?(node.name) 10 | nodes << node.to_s 11 | node.remove 12 | 13 | # Once the headings stop, dump them into an
. 14 | # Ah, and eat an
if there is one. 15 | else 16 | node.before("
#{nodes.join('')}
") if nodes.any? 17 | node.remove if node.name == 'hr' 18 | break 19 | end 20 | } 21 | 22 | html 23 | end 24 | end 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /lib/reacco/filters/literate.rb: -------------------------------------------------------------------------------- 1 | module Reacco 2 | module Filters 3 | module Literate 4 | # Moves pre's after headers. 5 | def move_pre(html) 6 | anchor = nil 7 | position = nil 8 | 9 | html.css('body>*').each_cons(2) { |(node, nxt)| 10 | # Once we find the
, move it.
11 |           if node.name == 'pre' && anchor && anchor != node && !(node['class'].to_s =~ /full/)
12 |             node.after "
" 13 | 14 | nxt['class'] = "#{nxt['class']} after-pre" if nxt 15 | node['class'] = "#{node['class']} right" 16 | 17 | anchor.send position, node 18 | anchor = nil 19 | 20 | # If we find one of these, put the next
 after it.
21 |           elsif %w(h1 h2 h3 h4 pre).include?(node.name)
22 |             anchor   = node
23 |             position = :add_next_sibling
24 | 
25 |           # If we find one of these, put the 
 before it.
26 |           elsif node['class'].to_s.include?('after-pre')
27 |             anchor   = node
28 |             position = :add_previous_sibling
29 |           end
30 |         }
31 | 
32 |         html
33 |       end
34 |     end
35 |   end
36 | end
37 | 


--------------------------------------------------------------------------------
/lib/reacco/filters/prelang.rb:
--------------------------------------------------------------------------------
 1 | module Reacco
 2 |   module Filters
 3 |     module PreLang
 4 |       # Adds prettify classes.
 5 |       def pre_lang(html)
 6 |         html.css('pre').each { |pre| pre['class'] = "#{pre['class']} lang-#{pre['class']} prettyprint" }
 7 |         html
 8 |       end
 9 |     end
10 |   end
11 | end
12 | 


--------------------------------------------------------------------------------
/lib/reacco/filters/sections.rb:
--------------------------------------------------------------------------------
 1 | module Reacco
 2 |   module Filters
 3 |     module Sections
 4 |       # Wraps in sections.
 5 |       def section_wrap(html)
 6 |         headings = %w(h1 h2 h3 h4 h5)
 7 |         headings.each do |h|
 8 |           nodes = html.css(h)
 9 |           nodes.each do |alpha|
10 |             # For those affected by --hgroup, don't bother.
11 |             next  if alpha.ancestors.any? { |tag| tag.name == 'hgroup' }
12 |             next  unless alpha.parent
13 | 
14 |             # Find the boundary, and get the nodes until that one.
15 |             omega         = from_x_until(alpha, *headings[0..headings.index(alpha.name)])
16 |             section_nodes = between(alpha, omega)
17 | 
18 |             # Create the 
. 19 | section = Nokogiri::XML::Node.new('section', html) 20 | section['class'] = "#{alpha['class']} #{h} #{slugify alpha.content}" 21 | alpha.add_previous_sibling(section) 22 | section_nodes.each { |tag| section.add_child tag } 23 | end 24 | end 25 | 26 | html 27 | end 28 | 29 | private 30 | def from_x_until(alpha, *names) 31 | omega = nil 32 | n = alpha 33 | 34 | while true 35 | n = n.next_sibling 36 | break if n.nil? 37 | 38 | name = n.name 39 | if name == 'section' 40 | name = (h = n.at_css('h1, h2, h3, h4, h5, h6')) && h.name 41 | end 42 | break if !name || names.include?(name) 43 | omega = n 44 | end 45 | 46 | omega 47 | end 48 | 49 | def between(first, last) 50 | nodes = Array.new 51 | started = false 52 | 53 | first.parent.children.each do |node| 54 | started = true if node == first 55 | nodes << node if started 56 | break if node == last 57 | end 58 | 59 | nodes 60 | end 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /lib/reacco/filters/toc.rb: -------------------------------------------------------------------------------- 1 | module Reacco 2 | module Filters 3 | module TOC 4 | def make_toc(contents) 5 | aside = Nokogiri::XML::Node.new('aside', contents) 6 | aside['id'] = 'toc' 7 | 8 | # Header 9 | title = (h1 = contents.at_css('h1')) && h1.text || File.basename(Dir.pwd) 10 | aside.inner_html = "#{aside.inner_html}

#{title}

" 11 | 12 | contents.xpath('//body/section').each { |tag| 13 | aside.inner_html = "#{aside.inner_html}#{make_section tag}" 14 | } 15 | 16 | contents.at_css('body').add_child aside 17 | 18 | contents 19 | end 20 | 21 | def linkify(h) 22 | href = slugify(h.content) 23 | "#{h.inner_html}" 24 | end 25 | 26 | def make_section(section) 27 | h = section.at_css('h1, h2, h3') 28 | return '' unless h 29 | level = h.name[1] # 1 | 2 | 3 30 | return '' unless %w(1 2 3).include?(level) 31 | name = h.content.strip 32 | 33 | out = case level 34 | when "1" 35 | [ "

#{linkify h}

", 36 | section.css('section.h2').map { |s| make_section s } 37 | ] 38 | when "2" 39 | [ "" 45 | ] 46 | when "3" 47 | [ "
  • #{linkify h}
  • " ] 48 | end 49 | 50 | out.flatten.join "\n" 51 | end 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /lib/reacco/generator.rb: -------------------------------------------------------------------------------- 1 | module Reacco 2 | class Generator 3 | def initialize(readme, dir, template=nil, css=nil) 4 | @readme = readme 5 | @dir = dir 6 | @css = css 7 | @template = template || Reacco.root('data') 8 | end 9 | 10 | # Returns the HTML contents. 11 | def html 12 | file = Dir["#{@template}/index.*"].first 13 | raise "No index file found in template path." unless file 14 | 15 | tpl = Tilt.new(file) 16 | out = tpl.render({}, @readme.locals) { @readme.html } 17 | end 18 | 19 | # Writes to the output directory. 20 | def write!(&blk) 21 | yield "#{@dir}/index.html" 22 | write_to "#{@dir}/index.html", html 23 | copy_files &blk 24 | append_css if @css 25 | end 26 | 27 | def template? 28 | File.directory?(@template) && Dir["#{@template}/index.*"].first 29 | end 30 | 31 | # Adds the CSS file to the style.css file. 32 | def append_css 33 | contents = File.read(@css) 34 | File.open("#{@dir}/style.css", 'a+') { |f| f.write "\n/* Custom */\n#{contents}" } 35 | end 36 | 37 | def copy_files(&blk) 38 | files = Dir.chdir(@template) { Dir['**/*'] } 39 | 40 | # For each of the template files... 41 | files.each do |f| 42 | next if File.basename(f)[0] == '_' 43 | next if File.fnmatch('index.*', f) 44 | ext = File.extname(f)[1..-1] 45 | 46 | fullpath = File.join @template, f 47 | 48 | # Try to render it with Tilt if possible. 49 | if Tilt.mappings.keys.include?(ext) 50 | contents = Tilt.new(fullpath).render 51 | outfile = f.match(/^(.*)(\.[^\.]+)$/) && $1 52 | else 53 | contents = File.read(fullpath) 54 | outfile = f 55 | end 56 | 57 | yield "#{@dir}/#{outfile}" 58 | write_to "#{@dir}/#{outfile}", contents 59 | end 60 | end 61 | 62 | private 63 | def write_to(path, data) 64 | FileUtils.mkdir_p File.dirname(path) 65 | File.open(path, 'w') { |f| f.write data } 66 | end 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /lib/reacco/readme.rb: -------------------------------------------------------------------------------- 1 | # ## Reacco::Readme [class] 2 | # A readme file. 3 | # 4 | # #### Instanciating 5 | # This is often instanciated by the `reacco` executable, but you can instantiate 6 | # it yourself like so. It's assumed the the base dir will be the current working 7 | # directory. 8 | # 9 | # readme = Reacco::Readme.new 10 | # readme = Reacco::Readme.new [OPTIONS HASH] 11 | # 12 | # #### Options 13 | # Avaiable options are *(all are optional)*: 14 | # 15 | # * `file` - String *(the filename of the README file)* 16 | # * `literate` - Boolean 17 | # * `toc` - Boolean 18 | # 19 | module Reacco 20 | class Readme 21 | def initialize(options={}) 22 | @file = options.delete(:file) if options[:file] 23 | @options = options 24 | end 25 | 26 | # ### file [method] 27 | # The path to the README file. Returns `nil` if not available. 28 | # 29 | # readme.file #=> "README.md" 30 | # 31 | def file 32 | @file ||= (Dir['README.*'].first || Dir['readme.*'].first || Dir['README'].first) 33 | end 34 | 35 | def file=(file) 36 | @file = file 37 | end 38 | 39 | # ### switches [method] 40 | # The switches, like *literate* and *toc*. Returns an array of strings. 41 | # 42 | # readme.switches #=> [ 'literate' ] 43 | def switches 44 | @options.keys.map { |k| k.to_s } 45 | end 46 | 47 | # ### exists? [method] 48 | # Returns true if the file (given in the constructor) exists. 49 | # 50 | # readme = Readme.new :file => 'non-existent-file.txt' 51 | # readme.exists? 52 | # #=> false (maybe) 53 | # 54 | def exists? 55 | file && File.exists?(file) 56 | end 57 | 58 | # ### raw [method] 59 | # Returns raw Markdown markup. 60 | # 61 | # readme.raw 62 | # #=> "# My project\n#### This is my project\n\n..." 63 | # 64 | def raw 65 | @raw ||= File.read(file) 66 | end 67 | 68 | # ### title [method] 69 | # Returns a string of the title of the document. Often, this is the first 70 | # H1, but if that's not available, then it is inferred from the current 71 | # directory's name. 72 | # 73 | # readme.title 74 | # #=> "My project" 75 | # 76 | def title 77 | @title ||= begin 78 | h1 = (h1 = doc.at_css('h1')) && h1.text 79 | h1 || File.basename(Dir.pwd) 80 | end 81 | end 82 | 83 | def title? 84 | title.to_s.size > 0 85 | end 86 | 87 | # ### raw_html [method] 88 | # Returns a string of the raw HTML data built from the markdown source. 89 | # This does not have the transformations applied to it yet. 90 | # 91 | # readme.raw_html 92 | # #=> "

    My project

    ..." 93 | # 94 | def raw_html 95 | @raw_html ||= markdown.render(raw).force_encoding('utf-8') 96 | end 97 | 98 | def raw_html=(str) 99 | @raw_html = str 100 | end 101 | 102 | # ### inject_api_block [method] 103 | # Adds an API block. Takes an `html` argument. 104 | # 105 | def inject_api_block(html) 106 | @api_blocks = "#{api_blocks}\n#{html}\n" 107 | end 108 | 109 | def api_blocks 110 | @api_blocks ||= "" 111 | end 112 | 113 | # ### doc([options]) [method] 114 | # Returns HTML as a Nokogiri document with all the transformations applied. 115 | # 116 | # readme.doc 117 | # #=> # 118 | # 119 | def doc(options={}) 120 | @doc ||= begin 121 | add_api(api_blocks) 122 | html = Nokogiri::HTML(raw_html) 123 | 124 | html = pre_lang(html) 125 | html = heading_id(html) 126 | html = wrap_hgroup(html) 127 | html = move_pre(html) if @options[:literate] 128 | html = brief_first_p(html) 129 | html = section_wrap(html) 130 | html = make_toc(html) if @options[:toc] 131 | 132 | html 133 | end 134 | end 135 | 136 | # ### html [method] 137 | # Returns body's inner HTML *with* transformatinos. 138 | # 139 | # readme.raw_html 140 | # #=> "

    My project

    ..." 141 | # 142 | def html 143 | doc.at_css('body').inner_html 144 | end 145 | 146 | # ### github [method] 147 | # Returns the GitHub URL, or nil if not applicable. 148 | # 149 | # readme.github 150 | # #=> "https://github.com/rstacruz/reacco" 151 | # 152 | def github 153 | "https://github.com/#{@options[:github]}" if @options[:github] 154 | end 155 | 156 | # ### locals [method] 157 | # Returns a hash with the local variables to be used for the ERB template. 158 | # 159 | # readme.locals 160 | # #=> { :title => 'My project', ... } 161 | # 162 | def locals 163 | { :title => title, 164 | :body_class => switches.join(' '), 165 | :analytics => @options[:analytics], 166 | :github => github } 167 | end 168 | 169 | private 170 | include Filters::Brief 171 | include Filters::Sections 172 | include Filters::Hgroup 173 | include Filters::Literate 174 | include Filters::PreLang 175 | include Filters::HeadingID 176 | include Filters::TOC 177 | 178 | # Puts `blocks` inside `raw_html`. 179 | def add_api(blocks) 180 | re1 = %r{^.*api reference goes here.*$}i 181 | re2 = %r{^.*href=['"]#api_reference['"].*$}i 182 | 183 | if raw_html =~ re1 184 | raw_html.gsub! re1, blocks 185 | elsif raw_html =~ re2 186 | raw_html.gsub! re2, blocks 187 | else 188 | self.raw_html = "#{raw_html}\n#{blocks}" 189 | end 190 | end 191 | 192 | # ### markdown [private method] 193 | # Returns the Markdown processor. 194 | # 195 | # markdown.render(md) 196 | # 197 | def markdown 198 | Reacco.markdown 199 | end 200 | 201 | # ### slugify [private method] 202 | # Turns text into a slug. 203 | # 204 | # "Install instructions" => "install_instructions" 205 | # 206 | def slugify(str) 207 | str.downcase.scan(/[a-z0-9\-]+/).join('_') 208 | end 209 | end 210 | end 211 | -------------------------------------------------------------------------------- /lib/reacco/version.rb: -------------------------------------------------------------------------------- 1 | module Reacco 2 | def self.version 3 | "0.0.3" 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /reacco.gemspec: -------------------------------------------------------------------------------- 1 | require './lib/reacco/version' 2 | 3 | Gem::Specification.new do |s| 4 | s.name = "reacco" 5 | s.version = Reacco.version 6 | s.summary = %{Readme prettifier.} 7 | s.description = %Q{Reacco makes your readme's pretty.} 8 | s.authors = ["Rico Sta. Cruz"] 9 | s.email = ["rico@sinefunc.com"] 10 | s.homepage = "http://github.com/rstacruz/reacco" 11 | s.files = `git ls-files`.strip.split("\n") 12 | s.executables = Dir["bin/*"].map { |f| File.basename(f) } 13 | 14 | s.add_dependency 'redcarpet', '~> 2.0.0b3' 15 | s.add_dependency 'nokogiri', '~> 1.5' 16 | s.add_dependency 'tilt' 17 | end 18 | -------------------------------------------------------------------------------- /test/blocks_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../test_helper', __FILE__) 2 | 3 | class BlocksTest < UnitTest 4 | setup do 5 | @ex = Reacco::Extractor.new(Dir[root 'lib/**/*.rb']) 6 | end 7 | 8 | should "extract comments" do 9 | # From Reacco.root 10 | block = @ex.blocks.detect { |blk| blk.title == "root" && blk.type == "class method" } 11 | assert ! block.nil? 12 | end 13 | 14 | should "htmlize properly" do 15 | block = @ex.blocks.detect { |blk| blk.title == "root" && blk.type == "class method" } 16 | p block.to_html 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | $:.unshift File.expand_path('../../lib', __FILE__) 2 | require 'contest' 3 | require 'reacco' 4 | 5 | class UnitTest < Test::Unit::TestCase 6 | def root(*a) 7 | Reacco.root *a 8 | end 9 | end 10 | --------------------------------------------------------------------------------