-
Notifications
You must be signed in to change notification settings - Fork 236
Custom tags
The upcoming JSDuck 5.0 brings a brand new custom tags system. It's much more flexible and powerful than the old system introduced in JSDuck 3.0, but it's not backwards compatible.
To introduce a new @tag
, you'll need to implement a Ruby class
extending from JsDuck::Tag::Tag. Then load the class into
JSDuck using the --tags
option:
$ jsduck --tags my_custom_tag.rb ...other.options...
But don't be scared of Ruby. We'll start off with simple boolean tags, that even a monkey can implement.
Let's define an @inner
tag, which we could use to label methods
which can't be accessed outside of class. For example:
/**
* Escapes regular expression metacharacters inside a string.
* @param {String} str Input string.
* @return {String} Escaped string.
* @inner
*/
function escapeRe(str) {
...
}
We call such tags boolean tags because they just either exist inside a doc-comment or not. And here's how to implement this one:
require "jsduck/tag/boolean_tag"
class Inner < JsDuck::Tag::BooleanTag
def initialize
@pattern = "inner"
@signature = {:long => "inner", :short => "in"}
super
end
end
Because boolean tag is such a common pattern, there is a special
BooleanTag class that we can extend. Inside the class we define an
initialize
method (a constructor in Ruby world), define some member
variables and call the superclass method to do some additional setup.
-
@pattern
defines a name of a tag to detect inside doc-comments. -
@signature
defines the labels to display in final documentation. The following screenshot should clarify the effects of it:
As you can see the :long
field defines the longer label to display
after member name, and :short
is for the label in dropdown menu.
But maybe just the label "inner" isn't quite enough. Let's add a little descriptive text to each inner method, to make things extra clear.
For this we need to implement a to_html
method on our tag class, and
define an @html_position
variable to mark the spot where we would
like our HTML to be injected:
require "jsduck/tag/boolean_tag"
class Inner < JsDuck::Tag::BooleanTag
def initialize
@pattern = "inner"
@signature = {:long => "inner", :short => "in"}
@html_position = POS_DOC + 0.1
super
end
def to_html(context)
"<p>This is an inner method, only accessible within the class itself.</p>"
end
end
Inside the to_html
method we just return some static HTML. to_html
also takes a parameter, but we'll ignore it for now.
The @html_position
is a more interesting beast. The
JsDuck::Tag::Tag class defines POS_*
constants for all the
builtin tags. When defining a custom tag, we want its HTML to be
positioned relative to the builtin tags. So we reference the
POS_DOC
which defines the position of the main documentation block
and add a small number to it to have the HTML of our tag be positioned
right after it.
The result will look as follows:
The above results looks quite lame though. There's just this gray label and line of text that's indistinguishable from the rest of the documentation. We need to spice things up with some CSS:
require "jsduck/tag/boolean_tag"
class Inner < JsDuck::Tag::BooleanTag
def initialize
@pattern = "inner"
@signature = {:long => "inner", :short => "in"}
@html_position = POS_DOC + 0.1
@css = <<-EOCSS
.signature .inner {
color: orange;
background: transparent;
border: 1px solid orange;
}
.inner-box {
border-color: orange;
}
EOCSS
super
end
def to_html(context)
<<-EOHTML
<div class='rounded-box inner-box'>
<p>This is an inner method, only accessible within the class itself.</p>
</div>
EOHTML
end
end
As one might guess we assign a string of CSS to the @css
field
(using ruby heredoc syntax).
.signature .inner
rule will assign styles to the small labels.
In to_html
we wrap our paragraph inside a div with two class names:
-
rounded-box
is a builtin helper class that turns the div into nice box woth rounded corners. -
inner-box
is a class we make up by ourselves to add some additional styles on top of whatrounded-box
provides.
The result will have a touch of orange, just as expected:
Now lets implement a @license
tag to describe how we license our
software. It should take as a parameter the name of the license:
/**
* @class My.Class
* An example class.
* @license GNU General Public License v3
* @license MIT License
*/
And here's an implementation for that:
require "jsduck/tag/tag"
class License < JsDuck::Tag::Tag
def initialize
@tagname = :license
@pattern = "license"
@html_position = POS_DOC + 0.1
end
def parse_doc(scanner)
text = scanner.match(/.*$/)
return { :tagname => :license, :text => text }
end
def process_doc(context, license_tags, position)
context[:license] = license_tags.map {|tag| tag[:text] }
end
def to_html(context)
licenses = context[:license].map {|license| "<b>#{license}</b>" }.join(" and ")
<<-EOHTML
<p>This software is licensed under: #{licenses}.</p>
EOHTML
end
end
There's quite a lot of stuff in here, so lets go over it all step-by-step.
First thing to note is that this time we inherit our tag class from
JsDuck::Tag::Tag. Previously we extended BooleanTag
, which
in turn extended the JsDuck::Tag::Tag
and performed a simple parsing
and processing behind the scenes. This time around we implement our
own parse_doc
and process_doc
methods.
def parse_doc(scanner)
text = scanner.match(/.*$/)
return { :tagname => :license, :text => text }
end
parse_doc
gets called every time the parser encounters our
@license
tag inside a doc-comment. JSDuck parses the name of the
tag by itself and then passes control over to parse_doc
to parse
anything after the tag name, passing in an instance of
JsDuck::Doc::Scanner on which we call the match
method
with a regex to extract all the text following the tag up to the end
of line.
For our purposes we have now done enough of parsing and successfully extracted the name of the license.
At this point we need to return a hash with our extracted data. The
hash must contain a :tagname
field which must match up with the
@tagname
variable we defined in initialize
. Anything else inside
this hash is our custom data we store there for later processing -
which in our case is just the name of the license.
def process_doc(context, license_tags, position)
context[:license] = license_tags.map {|tag| tag[:text] }
end
The hashes returned by parse_doc
within one doc-comment get grouped
together by the :tagname
we specified and passed to process_doc
.
In our example case the value of license_tags
parameter will be the
following:
[
{:tagname => :license, :text => "GNU General Public License"},
{:tagname => :license, :text => "MIT License"},
]
The task of the process_doc
method is to do any additional
processing on this data and save it into the context
hash (usually
under the same key as our :tagname
). In our case we just extract the
names of our licenses.
The third position
parameter contains file name and line number,
which can be used for error reporting.
def to_html(context)
licenses = context[:license].map {|license| "<b>#{license}</b>" }.join(" and ")
<<-EOHTML
<p>This software is licensed under: #{licenses}.</p>
EOHTML
end
This time inside to_html
we make use of the context
parameter,
which is the same context
that we modified in process_doc
. So we
just take the value we saved under :license
key and turn it into
HTML.
The result of this all will look as follows:
TODO...
parse_doc
gets passed an instance of
JsDuck::Doc::Scanner, which it must use to perform the
parsing. This Scanner is similar to Ruby builtin StringScanner,
it remembers the position of a scan pointer (a position inside the
string we're parsing). The scanning itself is a process of advancing
the scan pointer through the string a small step at a time.
Let's visualize how this works. Here's the state of the Scanner at
the time parse_doc
gets called.
* @license GNU General Public License v3
^
The scan pointer (denoted as ^
) has stopped at the beginning of the
word "GNU"
. At that point we could look ahead to see what's coming,
e.g. we could check if we're starting to parse a GNU license:
scanner.look(/GNU/) # --> true
The look
method performs a simple regex match starting at the
position of our scan pointer, returning true when the regex matches.
But just looking doesn't advance the scan pointer, that's what the
match
method is for:
scanner.match(/.*$/) # --> "GNU General Public License v3"
match
returns the actual string matching the regex and advances scan
pointer to the position where the match ended:
* @license GNU General Public License v3
^
The Scanner class contains a bunch of other useful methods for
parsing the docs, but look
and match
are really at the core of it,
and for our @license
tag purposes we have done enough of parsing and
successfully extracted the name of the license.
At this point we need to return a hash with our gathered data - otherwise we would get a tag that simply gets ignored.
The hash must contain a :tagname
field which must match up with the
@tagname
field we define in constructor. Anything else inside this
hash is our custom data we store there for later processing - which in
our case is just all the text that followed the @license
tag.
Let's define a @constant
tag. It should work pretty much like
@property
, but with the semantic difference of documenting
unchangable values. For example:
/**
* Acceleration of objects in Earth's gravitational field.
* @constant
*/
var ACCELERATION = 9.80665;
TODO...