Skip to content
Rene Saarsoo edited this page Feb 20, 2013 · 43 revisions

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.

Boolean tags

Let's define an @inner tag, which we could use 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:

Screenshot of @inner tag rendering

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.

Some HTML output

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:

Screenshot of @inner tag rendering with text

A touch of style

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 what rounded-box provides.

The result will have a touch of orange, just as expected:

Screenshot of @inner tag rendering with styles

Single-line tags

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.

parse_doc gets called every time the parser encounters our @license tag inside a doc-comment. It swallows the tag itself and then passes control over to parse_doc to parse anything after the tag.

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.

Custom member types

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...