Skip to content

Commit

Permalink
Merge pull request #502 from openannotation/docs-tools
Browse files Browse the repository at this point in the history
Add a couple of tools for generating API docs
  • Loading branch information
tilgovi committed Apr 9, 2015
2 parents edddbe8 + 2645c19 commit f40388a
Show file tree
Hide file tree
Showing 3 changed files with 262 additions and 0 deletions.
8 changes: 8 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ PLUGINS := \
unsupported
PLUGINS_PKG := $(patsubst %,pkg/annotator.%.js,$(PLUGINS))

SRC := $(shell find src -type f -name '*.js')

all: annotator plugins annotator-full

annotator: pkg/annotator.min.js pkg/annotator.min.css
Expand All @@ -34,6 +36,12 @@ develop:
doc:
cd doc && $(MAKE) html

apidoc: $(patsubst src/%.js,doc/api/%.rst,$(SRC))

doc/api/%.rst: src/%.js
@mkdir -p $(@D)
tools/apidoc $< >$@

pkg/%.min.css: pkg/%.css
@echo Writing $@
@$(UGLIFYCSS) $< >$@
Expand Down
68 changes: 68 additions & 0 deletions tools/apidoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
#!/bin/sh
#
# Usage: doc path/to/filename.js >output.rst
#
# doc generates reStructuredText documentation for JavaScript files. It is a
# thin wrapper over the js2rst tool, which extracts docstrings from the code.
#

set -eu

usage () {
echo "$(basename "$0") path/to/filename.js >output.rst"
}

abort () {
echo "$@, aborting" >&2
exit 1
}

skip () {
echo "$@, skipping" >&2
exit
}

pkgname () {
# Tell the docs what package a file is part of by putting a special comment
# in the source file that looks like:
#
# /*package some.package.name */
#
# Note the deliberately missing space.
fgrep '/*package ' "$1" | awk '{print $2}'
}

preamble () {
local pkgname=$1
local title="$pkgname package"
echo $title
echo $title | sed 's/./=/g'
echo
}

main () {
local src=${1:=}

if [ -z "$src" ]; then
usage
exit 1
fi

local ext=${src##*.}
if [ "$ext" != "js" ]; then
abort "only runs on .js files"
fi

local pkgname=$(pkgname "$src")
if [ -z "$pkgname" ]; then
skip "no package name found for $src"
fi


echo ".. default-domain: js"
echo
preamble $pkgname
tools/js2rst $pkgname <$src
}

main "$@"
186 changes: 186 additions & 0 deletions tools/js2rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
#!/usr/bin/env node
/*
* js2rst is a tool that parses JavaScript source supplied on STDIN and extracts
* documentation from documentation comments (those that start "/**").
*
* It prints reStructuredText to STDOUT.
*
* A single positional argument may be specified. If so, it is treated as the
* package name, and this is prefixed to any identifiers used in the
* documentation.
*/

"use strict";

var concat = require('concat-stream');
var esprima = require('esprima');

/**
* data:: pkgPrefix
*
* The package path in dotted form (e.g. "foo.bar.baz") to prefix to any
* identifiers found in the processed source.
*/
var pkgPrefix = null;
if (process.argv.length > 2) {
pkgPrefix = process.argv[2];
}

var processStream = concat(function (buf) {
processSource(buf.toString());
});

process.stdin.pipe(processStream);

/**
* function:: processSource(src)
*
* Read JavaScript source and print any documentation comments to STDOUT. See
* :func:`processComment` for more details.
*/
function processSource(src) {
var ast = esprima.parse(src, {
attachComment: true
});
for (var i = 0, len = ast.comments.length; i < len; i++) {
var comment = ast.comments[i];

if (comment.type !== "Block") {
continue;
}

// Document comments must start with "/**"
if (comment.value[0] !== "*") {
continue;
}

processComment(comment);
}
}

/**
* function:: processComment(comment)
*
* Read an esprima comment node and print it to STDOUT after suitable
* transformations:
*
* - remove leading whitespace and asterisks
* - prefix identifiers with the package prefix where appropriate
*/
function processComment(comment) {
// Normalise newlines
var lines = comment.value.split(/\r?\n/);
lines = dedent(lines);
lines = trimLines(lines);

if (lines.length === 0) {
return;
}

if (pkgPrefix !== null) {
var directiveMatch = /^([a-z:]+::)(.*)/;
var result = directiveMatch.exec(lines[0]);
var directive = result[1];
var identifier = result[2];
if (result !== null) {
lines[0] = directive + ' ' + pkgPrefix;
// If there is an identifier, join it with a dot to the package
// prefix.
if (typeof identifier !== 'undefined') {
identifier = identifier.trim();
if (identifier !== '') {
lines[0] += '.' + identifier;
}
}
}
}

process.stdout.write('.. ' + lines[0] + '\n');
process.stdout.write(indent(lines.slice(1), 4).join('\n') + '\n');
process.stdout.write('\n\n');
}

/**
* function:: dedent(lines)
*
* A little like Python's :py:mod:`textwrap.dedent`, this function will take the
* internal value of a docstring block comment and remove leading whitespace and
* stars, while also normalising the indent of each line to that of the least
* indented line.
*/
function dedent(lines) {
lines = lines.slice();

// Comment is assumed to be only the comment body, omitting the '/*' and
// '*/'. To make our regular expression simpler, we prepend to the comment
// so that alignment is preserved as in the original.
// li
if (lines.length > 0) {
lines[0] = '**' + lines[0];
}

var match = /^([*\s]*)[^*\s]/;
var trim = null;

// First pass determines trim size
for (var i = 0, len = lines.length; i < len; i++) {
var res = match.exec(lines[i]);
if (res === null) { continue; }

// If trim is set, check it's still the minimum indent size.
if (trim !== null) {
trim = Math.min(trim, res[1].length);
continue;
}

// Otherwise we just found the first line with content.
trim = res[1].length;
}

// Second pass for output
var output = [];
for (i = 0; i < len; i++) {
output.push(lines[i].slice(trim));
}
return output;
}

/**
* function:: indent(lines, count)
*
* Indent lines by count spaces.
*/
function indent(lines, count) {
var output = [];
var prefix = new Array(count + 1).join(' ');
for (var i = 0, len = lines.length; i < len; i++) {
output.push(prefix + lines[i]);
}
return output;
}

/**
* function:: trimLines(lines)
*
* Remove empty (or whitespace-only) lines from the beginning and end of a
* string.
*
* :param Array[String] lines: input lines
* :rtype: Array[String]
*/
function trimLines(lines) {
var len = lines.length;
var ws = /^\s*$/;
var start, end;
for (start = 0; start < len; start++) {
if (!ws.test(lines[start])) {
break;
}
}
for (end = len; end > start; end--) {
if (!ws.test(lines[end - 1])) {
break;
}
}
return lines.slice(start, end);
}

0 comments on commit f40388a

Please sign in to comment.