diff --git a/bin/judo b/bin/judo index 8c27814..22eb516 100755 --- a/bin/judo +++ b/bin/judo @@ -21,9 +21,16 @@ ap = ArgParseSettings("Generate documentation.") arg_type = String default = "default" help = "template to use" + "--package" + help = "build a package's documentation" + action = :store_true end args = ArgParse.parse_args(ap) -Judo.collate([args["filename"]], template=args["template"], outdir=args["out"]) +if args["package"] + Judo.collate(args["filename"]) +else + Judo.collate([args["filename"]], template=args["template"], outdir=args["out"]) +end diff --git a/doc/introduction.md b/doc/introduction.md new file mode 100644 index 0000000..f0d93fd --- /dev/null +++ b/doc/introduction.md @@ -0,0 +1,43 @@ +--- +title: Getting Started +author: Daniel Jones +... + +Judo is a Julia document generator. It takes documents written in +[pandoc markdown](http://johnmacfarlane.net/pandoc/README.html#pandocs-markdown) +and converts them into html, but differs from general purpose markdown tools in +a few ways. + + 1. Code blocks can be executed and their results inlined in the document, + including plots and graphics. + 2. Metadata can be attached to a document in the form of YAML front-matter + (similar to Jekyll). + 3. Multiple documents can be compiled and cross-linked. + 4. Function and types comments can be parsed from Julia source code and + included in a document. + +The end goal is to make documenting Julia code, whether it be a package, or some +quick-and-dirty analysis, as painless as possible. + + +# Installing + +Judo can be installed like a regular Julia package, but is used somewhat +differently. + +```{.julia execute="false"} +Pkg.add("Judo") +``` + +An executable `judo` script is now installed to +`joinpath(Pkg.dir("Judo"), "bin")` (typically `~/.julia/Judo/bin/judo`), which +you may want to add to your `PATH` variable. + + +# Using + +`judo -h` will give you some idea of how Judo is invoked. + + + + diff --git a/doc/markdown.md b/doc/markdown.md new file mode 100644 index 0000000..ae5f4b4 --- /dev/null +++ b/doc/markdown.md @@ -0,0 +1,85 @@ +--- +title: Judo Markdown +author: Daniel Jones +... + +Markdown interpreted by Judo is +[pandoc markdown](http://johnmacfarlane.net/pandoc/README.html#pandocs-markdown) +with some extensions. + +# YAML Front-Matter + +Markdown is great, but its often useful to be able to attach metadata to a +document. Judo borrows an idea from [Jekyll](http://jekyllrb.com/) to overcome +this. Before parsing a markdown document, Judo will look for a +leading [YAML](http://yaml.org/) document. + +Front-matter documents can be delineated with with `---`, indicating the +beginning of a YAML document, and `...`, indicating the end. This is standard +YAML, so document metadata can be parsed with any YAML parser. + +There is no restrictions on what is included in the metadata, but a few fields +are used directly by Judo. + + +## Metadata Fields + +`author`, `authors`: gives the name of the document's author or authors. + +`title`: gives the title of the document, which is used when generating a table +of contents. + +`data`: the date when the document was last modified. + +TODO: In the future more fields will be added, in particular `topics` to ease +the generation of an index. + + +# Executable Code Blocks + +Regular markdown code blocks are by default interpreted as Julia code and +executed. + +For example this code block will display its output inline in the compiled +version of this document. + +```julia +println("Hello world!") +``` + +## Controlling Execution + +Pandoc markdown allows attaching +[attributes](http://johnmacfarlane.net/pandoc/README.html#header-identifiers-in-html-latex-and-context) +to code blocks. These are used in Judo to control how code blocks are handled. + +The following key/value pairs are supported. + + * `execute="false"`: Do not execute the code block. + * `hide="true"`: Do not display the code block itself. + * `display="false"`: Do not display the output from executing the code block. + * `results="block"`: Display the result of the last expression in the block. + * `results="expression"`: Display the result of every expression in the block. + +Note: pandoc is pretty picky about how these are parsed. For example, quotation +marks around the value in key/value pairs are mandatory. + + +## Non-Julia Executables + +Not just Julia code can be executed. Code blocks can also be used to include +output from latex, graphiviz, or directly include svg code. Others can be added +easily. + + +```graphviz +digraph { + "write documentation" -> "compile documentation"; + "compile documentation" -> "enjoy an overwhelming sense of satisfaction"; +} +``` + + + + + diff --git a/src/Judo.jl b/src/Judo.jl index b08a2f7..41093d1 100644 --- a/src/Judo.jl +++ b/src/Judo.jl @@ -1,9 +1,10 @@ module Judo -import Base: start, next, done, display +import Base: start, next, done, display, writemime import JSON import YAML +import Mustache include("collate.jl") @@ -116,18 +117,38 @@ end # Display functions that render output and insert them into the document. -function display(doc::WeaveDoc, ::@MIME("text/plain"), data) + +const supported_mime_types = + { MIME("text/html"), + MIME("image/svg+xml"), + MIME("image/png"), + MIME("text/latex"), + MIME("text/vnd.graphviz"), + MIME("text/plain") } + + +function display(doc::WeaveDoc, data) + for m in supported_mime_types + if mimewritable(m, typeof(data)) + display(doc, m, data) + break + end + end +end + + +function display(doc::WeaveDoc, m::@MIME("text/plain"), data) block = {"CodeBlock" => - {{"", {"output"}, {}}, data}} + {{"", {"output"}, {}}, stringmime(m, data)}} push!(doc.display_blocks, block) end -function display(doc::WeaveDoc, ::@MIME("text/latex"), data) +function display(doc::WeaveDoc, m::@MIME("text/latex"), data) # latex to dvi input_path, input = mktemp() - print(input, data) + writemime(input, m, data) flush(input) seek(input, 0) latexout_dir = mktempdir() @@ -139,14 +160,14 @@ function display(doc::WeaveDoc, ::@MIME("text/latex"), data) output = readall(`dvisvgm --stdout --no-fonts $(latexout_path)` .> SpawnNullStream()) run(`rm -rf $(latexout_dir)`) - display("image/svg+xml", output) + display(doc, MIME("image/svg+xml"), output) end -function display(doc::WeaveDoc, ::@MIME("image/svg+xml"), data) +function display(doc::WeaveDoc, m::@MIME("image/svg+xml"), data) filename = @sprintf("%s_figure_%d.svg", doc.name, doc.fignum) - out = open(filename, "w") - write(out, data) + out = open(joinpath(doc.outdir, filename), "w") + writemime(out, m, data) close(out) alttext = @sprintf("Figure %d", doc.fignum) @@ -179,14 +200,49 @@ function display(doc::WeaveDoc, ::@MIME("image/svg+xml"), data) end -function display(doc::WeaveDoc, ::@MIME("text/vnd.graphviz"), data) +function display(doc::WeaveDoc, m::@MIME("image/png"), data) + filename = @sprintf("%s_figure_%d.png", doc.name, doc.fignum) + out = open(joinpath(doc.outdir, filename), "w") + writemime(out, m, data) + close(out) + + alttext = @sprintf("Figure %d", doc.fignum) + figurl = filename + caption = "" + + block = + {"Para" => + {{"Image" => + {{{"Str" => alttext}}, + {figurl, ""}}}}} + + doc.fignum += 1 + push!(doc.display_blocks, block) +end + + +function display(doc::WeaveDoc, m::@MIME("text/vnd.graphviz"), data) output, input, proc = readandwrite(`dot -Tsvg`) - write(input, data) + writemime(input, m, data) close(input) - display("image/svg+xml", readall(output)) + display(doc, MIME("image/svg+xml"), readall(output)) end +function display(doc::WeaveDoc, m::@MIME("text/html"), data) + block = {"RawBlock" => + {"html", stringmime(m, data)}} + push!(doc.display_blocks, block) +end + + +# This is maybe an abuse. TODO: This is going to be a problem. +writemime(io, m::@MIME("text/vnd.graphviz"), data::String) = write(io, data) +writemime(io, m::@MIME("image/vnd.graphviz"), data::String) = write(io, data) +writemime(io, m::@MIME("image/svg+xml"), data::String) = write(io, data) +writemime(io, m::@MIME("text/latex"), data::String) = write(io, data) + + # Transform a annotated markdown file into a variety of formats. # # This reads markdown input, optionally prefixed with a YAML metadata document, @@ -209,7 +265,8 @@ end # function weave(input::IO, output::IO; outfmt=:html5, name="judo", template=nothing, - toc=false, outdir=".") + toc::Bool=false, outdir::String=".", dryrun::Bool=false, + keyvals::Dict=Dict()) input_text = readall(input) # parse yaml front matter @@ -221,7 +278,8 @@ function weave(input::IO, output::IO; end # first pandoc pass - pandoc_metadata, document = JSON.parse(pandoc(input_text, :markdown, :json)) + pandoc_metadata, document = + JSON.parse(pandoc(input_text, :markdown, :json)) prev_stdout = STDOUT stdout_read, stdout_write = redirect_stdout() doc = WeaveDoc(name, outfmt, stdout_read, stdout_write, outdir) @@ -236,12 +294,16 @@ function weave(input::IO, output::IO; for subblock in nameblocks if subblock == "Space" write(name, " ") - elseif is(subblock, Dict) + elseif isa(subblock, Dict) write(name, subblock["Str"]) end end push!(sections, (level, takebuf_string(name))) - push!(doc.blocks, process_block(block)) + if !dryrun + push!(doc.blocks, process_block(block)) + end + elseif dryrun + continue elseif isa(block, Dict) && haskey(block, "CodeBlock") process_code_block(doc, block) else @@ -249,6 +311,10 @@ function weave(input::IO, output::IO; end end + if dryrun + return metadata, sections + end + # splice in metadata fields that pandoc supports pandoc_metadata_keys = ["title" => "docTitle", "authors" => "docAuthors", @@ -284,6 +350,10 @@ function weave(input::IO, output::IO; JSON.print(buf, {pandoc_metadata, doc.blocks}) args = {} + for (k, v) in keyvals + push!(args, "--variable=$(k):$(v)") + end + if template != nothing push!(args, "--template=$(template)") end @@ -428,12 +498,12 @@ function process_code_block(doc::WeaveDoc, block::Dict) end empty!(doc.display_blocks) else - if !hide + if !keyvals["hide"] push!(doc.blocks, block) end class = classes[1] - if display + if keyvals["display"] if class == "graphviz" display("text/vnd.graphviz", text) elseif class == "latex" @@ -441,24 +511,15 @@ function process_code_block(doc::WeaveDoc, block::Dict) elseif class == "svg" display("image/svg+xml", text) end + append!(doc.blocks, doc.display_blocks) end end end -# Evaluate an expression and return its results. Catch any errors and return -# them in string form. +# Evaluate an expression and return its result and a string. function safeeval(ex::Expr) - try - string(eval(WeaveSandbox, ex)) - catch e - errout = IOBuffer() - print(errout, "ERROR: ") - Base.error_show(errout, e) - result = bytestring(errout) - close(errout) - result - end + string(eval(WeaveSandbox, ex)) end diff --git a/src/collate.jl b/src/collate.jl index e0f971a..9a99db0 100644 --- a/src/collate.jl +++ b/src/collate.jl @@ -2,21 +2,62 @@ # Turn a collection of markdown files into a browsable multi-page manual. +# Recursively list files under a directory. +# +# Args: +# root: Path to descend. +# +# Returns: +# A vector of paths relative to root. +# +function walkdir(root::String) + root = abspath(root) + contents = String[] + stack = String[] + push!(stack, root) + while !isempty(stack) + path = pop!(stack) + for f in readdir(path) + fullpath = joinpath(path, f) + if isdir(fullpath) + push!(stack, fullpath) + else + push!(contents, fullpath) + end + end + end + contents +end + + +# Files to be weaved when generating a packages documentation. +const ext_doc_pat = r"\.(md|txt|rst)$"i + + function collate(package::String) + filenames = {} + for filename in walkdir(joinpath(Pkg.dir(package), "doc")) + if match(ext_doc_pat, filename) != nothing + push!(filenames, filename) + end + end + outdir = joinpath(Pkg.dir(package), "doc", "html") + if !isdir(outdir) + mkdir(outdir) + end + + collate(filenames, outdir=outdir, pkgname=package) end function collate(filenames::Vector; template::String="default", - outdir::String=".") - fileext_pat = r"^(.+)\.([^\.]+)$" - - # map topics to document names - topics = Dict{String, Vector{String}} - - # map document names to section names - sections = Dict{String, Vector{String}} + outdir::String=".", + pkgname=nothing) + toc = Dict() + titles = Dict() + names = Dict() if !isdir(template) template = joinpath(Pkg.dir("Judo"), "templates", template) @@ -25,32 +66,35 @@ function collate(filenames::Vector; end end + # dry-run to collect the section names in each document + for filename in filenames + metadata, sections = weave(open(filename), IOBuffer(), dryrun=true) + name = choose_document_name(filename) + title = get(metadata, "title", name) + titles[name] = title + names[title] = name + toc[title] = sections + end + pandoc_template = joinpath(template, "template.html") + keyvals = Dict() + + if pkgname != nothing + keyvals["pkgname"] = pkgname + end + for filename in filenames - mat = match(fileext_pat, filename) fmt = :markdown - name = filename - if !is(mat, nothing) - name = mat.captures[1] - if mat.captures[2] == "md" - fmt = :markdown - elseif mat.captures[2] == "rst" - fmt = :rst - elseif mat.captures[2] == "tex" - fmt = :latex - elseif mat.captures[2] == "html" || mat.captures[2] == "htm" - fmt = :html - else - name = filename - end - end - + name = choose_document_name(filename) + title = titles[name] outfilename = joinpath(outdir, string(name, ".html")) outfile = open(outfilename, "w") + keyvals["table-of-contents"] = table_of_contents(toc, names, title) metadata, sections = weave(open(filename), outfile, name=name, template=pandoc_template, - toc=true, outdir=outdir) + toc=true, outdir=outdir, + keyvals=keyvals) close(outfile) end @@ -60,3 +104,75 @@ function collate(filenames::Vector; end +# pattern for choosing document names +const fileext_pat = r"^(.+)\.([^\.]+)$" + + +# Choose a documents name from its file name. +function choose_document_name(filename::String) + filename = basename(filename) + mat = match(fileext_pat, filename) + name = filename + if !is(mat, nothing) + name = mat.captures[1] + if mat.captures[2] == "md" + fmt = :markdown + elseif mat.captures[2] == "rst" + fmt = :rst + elseif mat.captures[2] == "tex" + fmt = :latex + elseif mat.captures[2] == "html" || mat.captures[2] == "htm" + fmt = :html + else + name = filename + end + end + + name +end + + +# Generate a table of contents for the given document. +function table_of_contents(toc::Dict, names::Dict, selected_title::String) + out = IOBuffer() + write(out, "\n") + takebuf_string(out) +end + + +function table_of_contents_sections(sections) + if isempty(sections) + return "" + end + + out = IOBuffer() + write(out, "\n") + takebuf_string(out) +end + + +# Turn a section name into an html id. +function section_id(section::String) + lowercase(replace(section, r"\s+", "-")) +end + diff --git a/templates/default/css/custom.css b/templates/default/css/custom.css index 129a184..25b7b48 100644 --- a/templates/default/css/custom.css +++ b/templates/default/css/custom.css @@ -20,11 +20,17 @@ pre { background-color: inherit; color: inherit; margin: 0.5em 1.5em; - /*border-left: 1px solid #666;*/ font-family: "Source Code Pro", "monospace"; font-size: 9pt; } +code { + font-family: "Source Code Pro", "monospace"; + font-size: 9pt; + background-color: #f5f5f5; + color: inherit; +} + .output { background-color: #fafafa; border: 1px solid #f1f1f5; @@ -33,13 +39,15 @@ pre { h1 { padding-top: 0.75em; - margin-bottom: -2pt; + margin-bottom: 1.0pt; font-size: 18pt; font-family: "PT Sans", "sans-serif"; font-weight: bolder; } h2, h3, h4, h5 { + margin-bottom: 1.0pt; + font-size: 14pt; font-family: "PT Sans", "sans-serif"; font-weight: bolder; color: #555; @@ -68,43 +76,72 @@ a { font-size: 9pt; } -#TOC { +#table-of-contents { position: fixed; width: 300px; - height: 1600px; + height: 100%; background-color: #f7f7f7; border: 1px solid #f3f3f3; - padding: 2em; + overflow-y: scroll; + overflow-x: hidden; +} + +#table-of-contents-content { + padding-left: 35px; + padding-right: 35px; + padding-top: 35px; + position: absolute; } #title-block { padding-bottom: 1em; + text-align: right; } #title-block h1 { font-size: 36; } -#TOC ul { +#table-of-contents ul { list-style: none; padding: 0; padding-left: 0.5em; } -#TOC > li { +#table-of-contents > li { position: relative; display: block; } -#TOC li > a { +#table-of-contents li > a { font-weight: normal; display: block; } -#TOC li > a:hover, -#TOC li > a:focus { +#table-of-contents li a:hover, +#table-of-contents li a:focus { text-decoration: none; background-color: #eeeeee; } +.toc-current-doc { + font-size: 16pt; +} + +.toc-current-doc a { + font-weight: normal; +} + +#pkgname { + width: 230px; + text-align: center; + text-shadow: 0px 2px 3px #ccc; + color: #555; +} + +#pkgname h1 { + font-size: 52pt; + padding: 0; +} + diff --git a/templates/default/js/inflateText.js b/templates/default/js/inflateText.js new file mode 100644 index 0000000..2910a34 --- /dev/null +++ b/templates/default/js/inflateText.js @@ -0,0 +1,80 @@ +/*global jQuery */ +/** + * InflateText.js -- 98% derived from FitText.js (http://fittextjs.com) + * + * Options + * - scale {Number} Scaling factor for the final font-size (defaults to 1) + * - minFontSize {Number} + * - maxFontSize {Number} + * + * @author RJ Zaworski ') + .appendTo('body'), + test = $this.clone().css({ + display:'inline', + fontSize:'96px' + }).appendTo(mask); + + // scale font down to fix IE bug + $this.css('font-size','12pt'); + + // update width + $this.css('font-size', Math.max(Math.min((settings.scale * 96 * $this.width() / test.width()), parseFloat(settings.maxFontSize)),parseFloat(settings.minFontSize))); + + // remove test element from DOM + mask.remove(); + } + + // Call once to set. + resizer(); + + // Call on resize. Opera debounces their resize by default. + $(window).resize(_debounce(resizer) ); + }); + }; +})( jQuery ); \ No newline at end of file diff --git a/templates/default/template.html b/templates/default/template.html index a8a1a2b..0fea342 100644 --- a/templates/default/template.html +++ b/templates/default/template.html @@ -10,23 +10,29 @@ +
+
+

$title$

+
- $body$ +$body$
-
-
-

$title$

+
+
+
+

$pkgname$

+
+ $table-of-contents$
- $toc$
@@ -35,6 +41,11 @@

$title$

Last modified by $author$ on $date$. Generated with Judo.

+ + +