Commit 8579a406 authored by Rene Saarsoo's avatar Rene Saarsoo
Browse files

Extract inline examples on Ruby side.

Created InlineExamples class that extracts examples from already
formatted code and writes them to inline-examples.js file.

The front-end now only needs to load this one file with all examples in
it.
parent 8e344091
Loading
Loading
Loading
Loading
+5 −0
Original line number Diff line number Diff line
@@ -20,6 +20,7 @@ require 'jsduck/index_html'
require 'jsduck/api_exporter'
require 'jsduck/full_exporter'
require 'jsduck/app_exporter'
require 'jsduck/inline_examples'
require 'fileutils'

module JsDuck
@@ -73,6 +74,10 @@ module JsDuck
        end
        format_classes

        if @opts.doctests
          InlineExamples.new(@relations).write(@opts.output_dir+"/inline-examples.js")
        end

        cw = ClassWriter.new(AppExporter, @relations, @opts)
        cw.write(@opts.output_dir+"/output", ".js")

+56 −0
Original line number Diff line number Diff line
require 'jsduck/json_duck'
require 'cgi'

module JsDuck

  # Extracts inline examples from formatted docs and writes to file
  class InlineExamples
    def initialize(relations)
      @begin_example_re = /<pre class='inline-example ([^']*)'><code>/
      @end_example_re = /<\/code><\/pre>/

      @examples = []
      relations.each do |cls|
        extract(cls[:doc]).each_with_index do |ex, i|
          @examples << {
            :id => cls.full_name + "-" + i.to_s,
            :name => cls.full_name + " example #" + (i+1).to_s,
            :href => '#!/api/' + cls.full_name,
            :code => ex,
          }
        end
      end
    end

    def write(filename)
      JsonDuck.write_jsonp(filename, "__inline_examples__", @examples)
    end

    # Extracts inline examples from HTML
    def extract(html)
      examples = []

      s = StringScanner.new(html)
      while !s.eos? do
        if s.check(/</)
          if s.check(@begin_example_re)
            s.scan(@begin_example_re)
            ex = s.scan_until(@end_example_re).sub(@end_example_re, '')
            examples << CGI.unescapeHTML(strip_tags(ex))
          else
            s.skip(/</)
          end
        else
          s.skip(/[^<]+/)
        end
      end

      examples
    end

    def strip_tags(str)
      str.gsub(/<.*?>/, "")
    end
  end

end
+72 −0
Original line number Diff line number Diff line
require "jsduck/inline_examples"
require "jsduck/doc_formatter"

describe JsDuck::InlineExamples do

  def extract(doc)
    html = JsDuck::DocFormatter.new.format(doc)
    JsDuck::InlineExamples.new([]).extract(html)
  end

  it "finds no examples from empty string" do
    extract("").should == []
  end

  it "finds no examples from simple text" do
    extract("bla bla bla").should == []
  end

  it "finds no examples from code blocks without @example tag" do
    extract(<<-EOS).should == []
Here's some code:

    My code

    EOS
  end

  it "finds one single-line example" do
    extract(<<-EOS).should == ["My code\n"]
    @example
    My code
    EOS
  end

  it "finds one multi-line example" do
    extract(<<-EOS).should == ["My code\n\nMore code\n"]
    @example
    My code

    More code
    EOS
  end

  it "finds two examples" do
    extract(<<-EOS).should == ["My code 1\n", "My code 2\n"]
First example:

    @example
    My code 1

And another...

    @example
    My code 2
    EOS
  end

  it "preserves HTML inside example" do
    extract(<<-EOS).should == ["document.write('<b>Hello</b>');\n"]
    @example
    document.write('<b>Hello</b>');
    EOS
  end

  it "ignores links inside examples" do
    JsDuck::InlineExamples.new([]).extract(<<-EOS).should == ["Ext.define();\n"]
<pre class='inline-example '><code><a href="#!/api/Ext">Ext</a>.define();
</code></pre>
EOS
  end

end
+9 −101
Original line number Diff line number Diff line
@@ -5,24 +5,6 @@ Ext.define('Docs.controller.DocTests', {
    extend: 'Docs.controller.Content',
    baseUrl: "#!/doctests",

    /**
     * Regex used to locate all `pre` nodes.
     * @private
     */
    preRegex: /<pre[\s\S]*?>[\s\S]*?<\/pre[\s\S]*?>/gi,

    /**
     * Regex used to determine if a node is an inline example.
     * @private
     */
    preClsRegex: /class=[\s\S]*?inline-example/i,

    /**
     * Regex used to locate `code` node.
     * @private
     */
    codeRegex: /<code[\s\S]*?>[\s\S]+?<\/code[\s\S]*?>/gi,

    refs: [
        {
            ref: 'viewport',
@@ -39,7 +21,7 @@ Ext.define('Docs.controller.DocTests', {

        this.control({
            '#doctestsgrid': {
                afterrender: this.locateExamples
                afterrender: this.loadExamples
            }
        });
    },
@@ -59,95 +41,21 @@ Ext.define('Docs.controller.DocTests', {
    },

    /**
     * Locates all examples.
     * Loads all examples from .js file.
     * @private
     */
    locateExamples: function() {
        this.classesLeft = Docs.data.classes.length;
    loadExamples: function() {
        this.getIndex().disable();
        Ext.each(Docs.data.classes, function(cls) {
            this.locateClsExamples(cls.name);
        }, this);
    },

    /**
     * Locates all inline examples attached to a class file.
     *
     * @param {String} cls The Ext class name being interrogated.
     * @private
     */
    locateClsExamples: function(cls) {
        var baseUrl = this.getBaseUrl() + '/output/',
            url = baseUrl + cls + '.js';

        Ext.data.JsonP.request({
            url: url,
            callbackName: cls.replace(/\./g, '_'),
            success: function(json, opts) {
                var exampleCodes = this.extractExampleCode(json.html),
                    exampleCodeLength = exampleCodes.length;
                Ext.each(exampleCodes, function(exampleCode, exampleIdx) {
                    var name = json.name,
                        id = name;
                    if (exampleCodeLength > 1) {
                        name += ' example #' + (exampleIdx + 1).toString();
                        id += '-' + exampleIdx.toString();
                    }

                    this.getIndex().addExample({
                        id: id,
                        name: name,
                        href: document.location.href.replace(/#.*/, '#!/api/' + json.name),
                        code: exampleCode,
                        status: 'ready'
                    });
                }, this);

                this.countClassLoaded();
            },
            failure: function(response, opts) {
                this.getIndex().addExample({
                    id: cls,
                    name: cls,
                    href: document.location.href.replace(/#.*/, '#!/api/' + cls),
                    code: 'ClassLoadingFailed();',
                    status: 'failure',
                    message: 'Class loading failed'
                });

                this.countClassLoaded();
            url: this.getBaseUrl() + "/inline-examples.js",
            callbackName: "__inline_examples__",
            success: function(examples) {
                this.getIndex().addExamples(examples);
                this.getIndex().enable();
            },
            scope: this
        });
    },

    // Counts that yet another class has been loaded.
    // When all loaded, enable the page again.
    countClassLoaded: function() {
        this.classesLeft--;
        if (this.classesLeft === 0) {
            this.getIndex().enable();
    }
    },

    /**
     * Extract example code from html.
     *
     * @param {String} html The html being parsed.
     * @private
     */
    extractExampleCode: function(html) {
        var exampleCodes = [],
            preMatches = html.match(this.preRegex);

        Ext.each(preMatches, function(preMatch) {
            if (preMatch.match(this.preClsRegex)) {
                var codeMatches = preMatch.match(this.codeRegex);
                if (codeMatches) {
                    exampleCodes.push(codeMatches[0]);
                }
            }
        }, this);
        return exampleCodes;
    }
});
+2 −3
Original line number Diff line number Diff line
@@ -8,8 +8,7 @@ Ext.define('Docs.model.DocTest', {
        'name',
        'href',
        'code',
        'status',
        'message',
        'trace'
        {name: 'status', defaultValue: 'ready'},
        'message'
    ]
});
Loading