Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

split in two for browser and node, too much boilerplate to keep them …

…in the same file
  • Loading branch information...
commit 3ae273cd60a077108b2477010b772f894fa39e6a 1 parent 7ecd16d
@debounce debounce authored
View
8 Readme.md
@@ -48,6 +48,14 @@ Running tests
Run /tests/specrunner.html in your favourite browser.
Run node.js test with the command
+TODO
+====
+1. add tests to verify the correctness of the actual output
+2. currenty the output does not preserve the ending chars of the original sentences
+3. make the lib more plugable:
+- allow plugin of custom algorithms
+- better control for the length of the summary, by words, by letters
+4. make it work in the browser! Tests are already in the
Licence
=======
View
3  lib/stemmer.js → lib/porter-stemmer.js
@@ -184,3 +184,6 @@ var stemmer = (function(){
return w;
}
})();
+
+// convert for usage with nodejs. added by alex topliceanu, 22.12.2011
+exports = stemmer;
View
1  package.json
@@ -15,6 +15,7 @@
"dependencies": {
"underscore": "~1.2.2",
"underscore.string": "~2.0.0",
+ "porter-stemmer": "~0.9.1",
"vows": "~0.5.13"
},
}
View
162 sum.browser.js
@@ -0,0 +1,162 @@
+//make sure that underscore.string is installed
+_.mixin( _.str.exports() );
+
+(function (_undef) {
+ "use strict";
+
+ //default values
+ var defaults = {
+ nSentences: 1,
+ exclude: [],
+ emphasise: []
+ };
+
+ // regexes
+ var sentenceDelimiter = /[.!?;]/;
+ var wordDelimiter = /\s/mg;
+ var matchJunk = /["#$%&'()*+,\-\/:<=>@\[\\\]\^_`{|}]/mg ;
+
+ var stopWords = ["", "a", "about", "above", "above", "across", "after", "afterwards", "again", "against", "all", "almost", "alone", "along", "already", "also","although","always","am","among", "amongst", "amoungst", "amount", "an", "and", "another", "any","anyhow","anyone","anything","anyway", "anywhere", "are", "around", "as", "at", "back","be","became", "because","become","becomes", "becoming", "been", "before", "beforehand", "behind", "being", "below", "beside", "besides", "between", "beyond", "bill", "both", "bottom","but", "by", "call", "can", "cannot", "cant", "co", "con", "could", "couldnt", "cry", "de", "describe", "detail", "do", "done", "down", "due", "during", "each", "eg", "eight", "either", "eleven","else", "elsewhere", "empty", "enough", "etc", "even", "ever", "every", "everyone", "everything", "everywhere", "except", "few", "fifteen", "fify", "fill", "find", "fire", "first", "five", "for", "former", "formerly", "forty", "found", "four", "from", "front", "full", "further", "get", "give", "go", "had", "has", "hasnt", "have", "he", "hence", "her", "here", "hereafter", "hereby", "herein", "hereupon", "hers", "herself", "him", "himself", "his", "how", "however", "hundred", "ie", "if", "in", "inc", "indeed", "interest", "into", "is", "it", "its", "itself", "keep", "last", "latter", "latterly", "least", "less", "ltd", "made", "many", "may", "me", "meanwhile", "might", "mill", "mine", "more", "moreover", "most", "mostly", "move", "much", "must", "my", "myself", "name", "namely", "neither", "never", "nevertheless", "next", "nine", "no", "nobody", "none", "noone", "nor", "not", "nothing", "now", "nowhere", "of", "off", "often", "on", "once", "one", "only", "onto", "or", "other", "others", "otherwise", "our", "ours", "ourselves", "out", "over", "own","part", "per", "perhaps", "please", "put", "rather", "re", "same", "see", "seem", "seemed", "seeming", "seems", "serious", "several", "she", "should", "show", "side", "since", "sincere", "six", "sixty", "so", "some", "somehow", "someone", "something", "sometime", "sometimes", "somewhere", "still", "such", "system", "take", "ten", "than", "that", "the", "their", "them", "themselves", "then", "thence", "there", "thereafter", "thereby", "therefore", "therein", "thereupon", "these", "they", "thickv", "thin", "third", "this", "those", "though", "three", "through", "throughout", "thru", "thus", "to", "together", "too", "top", "toward", "towards", "twelve", "twenty", "two", "un", "under", "until", "up", "upon", "us", "very", "via", "was", "we", "well", "were", "what", "whatever", "when", "whence", "whenever", "where", "whereafter", "whereas", "whereby", "wherein", "whereupon", "wherever", "whether", "which", "while", "whither", "who", "whoever", "whole", "whom", "whose", "why", "will", "with", "within", "without", "would", "yet", "you", "your", "yours", "yourself", "yourselves", "the"];
+
+ // function used to clean sentences before splitting into words
+ var clean = function (str) {
+ return _(str).chain()
+ .unescapeHTML()
+ .stripTags()
+ .clean()
+ .value()
+ .replace( matchJunk, '' )
+ .toLowerCase();
+ };
+
+ // Sentence Module
+ var Sentence = function (s) {
+ var c = clean( s );
+ var all = _.words( c, wordDelimiter );
+ var words = _(all).chain()
+ // remove stop words
+ .filter( function (w) {
+ return (stopWords.indexOf( w ) === -1) ;
+ })
+ // apply stemmer
+ .map( function (w) {
+ return stemmer( w );
+ })
+ // collect word frequencies
+ .reduce( function (collect, w) {
+ collect[w] = collect[w] ? collect[w] + 1 : 1 ;
+ return collect;
+ }, {}).value();
+ // remove a word from this sentence to reduce redundancy in results
+ var remove = function (w) {
+ return delete words[w];
+ };
+ return {
+ orig: s,
+ words: words,
+ remove: remove
+ };
+ };
+
+ var sum = function (opts){
+
+ // handle options
+ opts = _.extend( {}, defaults, opts );
+ opts.corpus = opts.corpus || _undef;
+ if (opts.corpus === _undef) throw Error( 'No input corpus' );
+
+ // clean corpus
+ var s = opts.corpus.split( sentenceDelimiter ); // TODO: keep the sentence ending chars
+ var sentences = _(s).map( function (s) {
+ return new Sentence(s);
+ });
+
+ // return all sentences that contain a givven word
+ var containing = function (w) {
+ return _(sentences).filter( function (s) {
+ return (s.words[w] !== undefined) ;
+ });
+ };
+
+ // if summary must exclude words in opts.exclude remove sentences that contain those words
+ if ( _.isArray(opts.exclude) && opts.exclude.length !== 0) {
+ var excludes = _(opts.exclude).map( function (w) {
+ return stemmer(clean(w));
+ });
+ sentences = _(sentences).filter( function (s) {
+ var words = _(s.words).keys();
+ return (_.intersection( words, excludes ).length === 0);
+ });
+ }
+
+
+ var summary = [] ;
+ var counter = 0;
+
+ // extract sentences in order of their relevance
+ while (true) {
+ var N = sentences.length;
+
+ // builds a hash of all words with global frequencies
+ var words = _(sentences).reduce( function (collect,s) {
+ _(s.words).each( function (count, w) {
+ collect[w] = collect[w] ? collect[w] + count : count ;
+ });
+ return collect;
+ }, {});
+
+ // if summary must have the words in opts.emphasise
+ var emphasise = [];
+ if ( _.isArray(opts.emphasise) && opts.emphasise.length !== 0) {
+ emphasise = _(opts.emphasise).map( function (w) {
+ return stemmer(clean(w));
+ });
+ }
+
+ //calculate relevance for each sentence
+ _(sentences).each( function (s) {
+ var relevance = _(s.words).reduce( function (memo, freq, w) {
+ var local = Math.log( 1 + freq );
+ var global = Math.log( N / containing(w).length );
+ return memo = memo + (local * global);
+ }, 0);
+
+ // if current sentence containes emphasised words, bumb up the relevance
+ var bump = _.intersection(emphasise, _(s.words).keys()).length;
+ relevance += bump * 1000; //big enough to push it in front
+
+ s.relevance = relevance;
+ })
+
+ // highest relevance sentence
+ var highest = _(sentences).max( function (s) {
+ return s.relevance;
+ });
+
+ // remove words from the remaining sentences to reduce redundancy
+ sentences = _(sentences).chain()
+ .without(highest)
+ .map( function (s) {
+ _(highest.words).each( function (w) {
+ s.remove( w );
+ });
+ return s;
+ })
+ .value();
+
+ summary.push( highest.orig ) ;
+ counter += 1;
+
+ var stop = (counter === opts.nSentences || sentences.length === 0);
+ if (stop) break;
+ }//~ end while
+ return {
+ 'summary': summary.join('.'),
+ 'sentences': summary
+ };
+ };
+
+ //public api
+ window.sum = sum ;
+
+}).call(this);
View
233 sum.js
@@ -1,21 +1,10 @@
-(function () {
- "use strict";
-
- // make the module usable both in browsers and in node.js
- var exports, _undef, _ ;
- if ( typeof module !== 'undefined' && typeof module.exports !== 'undefined' ) {
- exports = module.exports;
- _ = require('underscore');
- _.str = require('underscore.string');
- }
- else {
- exports = this;
- _ = this._;
- }
-
- // make sure underscore.mixin is installed
- _.mixin( _.str.exports() );
+var stemmer = require( 'porter-stemmer' ).stemmer;
+var _ = require( 'underscore' );
+_.str = require( 'underscore.string' );
+_.mixin( _.str.exports() );
+(function (_undef) {
+ "use strict";
//default values
var defaults = {
@@ -170,214 +159,6 @@
};
//public api
- exports.sum = sum ;
+ module.exports = sum;
}).call(this);
-
-
-
-
-
-
-
-
-
-
-
- /*
-
- (function () {
- if (window !== undefined) {
- window._.mixin( window._.str.exports() );
- return { // for browsers
- 'exports': window,
- _: window._
- };
- }
- if (module && module.exports) {
- var _ = require( 'underscore' );
- _.str = require( 'underscore.string' );
- _.mixin( _.str.exports() );
- return { // for nodejs
- 'exports': module.exports,
- '_': _
- };
- }
- else if (window !== undefined) {
- window._.mixin( window._.str.exports() );
- return { // for browsers
- 'exports': window,
- _: window._
- };
- }
- throw Exception( 'Unsupported Environment!' );
-})());
-
-
-
-
-
-/*
-// TODO register cleanup function ;)
-var sum = (function(win,undef){
-
- "use strict" ;
-
- var newWord = function( word ) {
- if( typeof word !== 'string' ){
- return undef ;
- }
- return {
- 'word' : word ,
- 'gwf' : 1
- } ;
- } ;
-
- var newSentence = function( sentence, cleanSentence ) {
- if( typeof sentence !== 'string' && typeof cleanSentence !== 'string' ){
- return undef ;
- }
- return {
- sentence : sentence ,
- cleanSentence : cleanSentence ,
- wordHash : {} ,
- lwf : {}
- } ;
- } ;
-
- var matchMultipleSpaces = /\s{2,}/mg ;
- var sentenceSplitter = /[.!?;]/ ;
- var wordSplitter = /\s/mg ;
-
- var localWeight = function( wordObj, sentenceObj ) {
- //return Math.log( 1 + sentenceObj.lwf[ wordObj.word ] ) ;
- var maxFreq = 0 ;
- for( var w in sentenceObj.wordHash ) {
- if( maxFreq < sentenceObj.lwf[w] ){
- maxFreq = sentenceObj.lwf[w] ;
- }
- }
- return (1+sentenceObj.lwf[w]/maxFreq)/2 ;
- } ;
-
-
- var sum = function( corpus , params ) { // params.size - nr of sentences in output
-
- // defaults
- params = params || {} ;
- params.size = params.size || 1 ;
-
-
- var wordHash = {} ; // global word hash
- var sentenceHash = {} ;
-
- // clean version of the corpus
- var cleanCorpus = corpus
- .replace( matchJunk , '' ) // leaves out .!?; and all spaces
- .replace( matchMultipleSpaces , ' ' ) ; // removes multiple spaces and keeps just one
-
- var sentences = corpus.split( sentenceSplitter ).filter( function(sentence){
- return ( sentence !== ' ' && sentence !== '' ) ; //TODO externalize
- }) ;
- var N = sentences.length ;
- var cleanSentences = cleanCorpus.split( sentenceSplitter ).filter( function(sentence){
- return ( sentence !== ' ' && sentence !== '' ) ; //TODO externalize
- });
- //TODO check if cleanSentences.length === N. if not throw an error
-
- for( var i = 0, len = sentences.length ; i < len ; i ++ ) {
- var s = sentences[ i ] ;
- var cs = cleanSentences[ i ] ;
- var sent = sentenceHash[ s ] = newSentence( s, cs ) ;
- var cleanWords = cs.split( wordSplitter ).filter( function(word){ //TODO externalize word filter func
- return ( word !== ' ' && word !== '' ) ;
- }) ;
- for( var j = 0, lenW = cleanWords.length ; j < lenW ; j ++ ) {
- var w = cleanWords[j] ;
- if( wordHash[ w ] === undef ) {
- wordHash[ w ] = newWord( w ) ;
- }
- else {
- wordHash[ w ].gwf ++ ; // increase global word frequency
- }
- if( sent.wordHash[ w ] === undef ) {
- sent.wordHash[ w ] = wordHash[ w ] ;
- sent.lwf[ w ] = 1 ;
- }
- else {
- sent.lwf[ w ] ++ ;
- }
- }
- }
-
- var removeSentence = function( sentenceObj ) { // param: the sentence object
- for( w in sentenceObj.wordHash ) {
- if( sentenceObj.wordHash.hasOwnProperty( w ) ){
- wordHash[w] = undef ; // removes from all sentences and global word hash
- }
- }
- sentenceHash[ sentenceObj.cleanSentence ] = undef ;
- N = N - 1 ;
- } ;
-
-
- var globalWeight = function( wordObj ){
- var ni = 0 ;
- for( var i in sentenceHash ) {
- if( sentenceHash.hasOwnProperty( i ) ){
- var s = sentenceHash[i] ;
- if( s.wordHash[ wordObj.word ] !== undef ){
- ni ++ ;
- }
- }
- }
- return Math.log( N / ni ) ;
- } ;
-
- var relevanceScore = function( sentenceObj ) { //TODO momoization wouldn't hurt
- var score = 0, w, lw, gw ;
- for( var i in sentenceObj.wordHash ) {
- if( sentenceObj.wordHash.hasOwnProperty( i ) ) {
- w = sentenceObj.wordHash[i] ;
- lw = localWeight( w , sentenceObj ) ;
- gw = globalWeight( w ) ;
- score += lw * gw ;
- }
- }
- return score ;
- } ;
-
- var summary = '' ;
-
- for( var k = 1 ; k <= params.size ; k ++ ) {
- // 1. sort descending sententece by relevance score
- sentences.sort( function( s1, s2 ){
- var rs1 = relevanceScore( sentenceHash[ s1 ] ) ;
- var rs2 = relevanceScore( sentenceHash[ s2 ] ) ;
- if( rs1 < rs2 ) {
- return 1 ;
- }
- else {
- if( rs1 > rs2 ) {
- return -1 ;
- }
- else {
- return 0 ;
- }
- }
- }) ;
-
- // 2. remove sentence
- var topScoringSentence = sentences.splice(0,1) ;
- removeSentence( topScoringSentence ) ;
-
- // 3. add to summary
- summary += topScoringSentence ;
- }
-
- return summary ;
- } ; //~ end sum()
-
- return sum ;
-})() ;
-*/
View
7 tests/browser/corpus.js
@@ -1,7 +0,0 @@
-var corpus = "Ben Nadel's Web Development Projects I have decided to get rid of my Snippets and Portfolio sections. I never put anything into the portfolio and I pretty much stopped maintaining my snippets. I found, instead, that I put most of my code snippets directly into my blog entries. Now, instead of putting little bits of code into snippets, I will only maintain several larger projects in this section.CFHTTPSession.cfc "+
-"The CFHttpSession.cfc is a ColdFusion component that wraps around multiple CFHttp requests in such a way that cookie and session information is maintained from request to request. This allows you to use this ColdFusion component to log into remote systems and grab content that is behind a layer of security. ColdFusion Builder Extension: RegEx Find And Replace This ColdFusion Builder provides regular expression (RegEx) find and replace functionality with a very usable interface and full Java regular expression support."+
-" It allows you to preview find/replace actions before committing them. It also allows you to perform multiple fine/replace actions on a single file without leaving the interface. CorMVC jQuery Framework CorMVC is a jQuery-powered Model-View-Controller (MVC) framework that can aide in the development of single-page, web-based applications. CorMVC stands for client-only-required model-view-controller and is designed to be lowest possible entry point to learning about single-page application architecture. It does not presuppose any server-side technologies, or a web server of any kind, and requires no more than a web browser to get up and running. CSSRule.cfc The CSSRule.cfc is a ColdFusion component that can parse and model CSS using ColdFusion."+
-" I love CSS; I think it's like the most awesome, straightforward way to describe formatting of rendered boxes. As such, I wanted to use it in more of my projects. However, in order to do this, I needed to come up with a way to parse and maintain the CSS model programmatically inside of ColdFusion. jQuery Photo Tagger Plugin The jQuery Photo Tagger plugin allows you to add Flickr-style photo tagging to your images. You can create box-like overlays on top of your image and assign messages to each box. The plugin communicates with the server using the supplied API URLs such that the photo tags can be saved, persisted, and deleted when needed."+
-" jQuery Template Markup Language (JTML) The jQuery Template Markup Language, or JTML, is a jQuery-powered rendering engine that allows you to use Script tags to easily populate HTML templates with complex data objects using JTML tags. The Kinky Calendar System is a totally free ColdFusion calendar system that was designed, not as a stand-alone application, but rather as a module that could be easily integrated into an existing piece of ColdFusion software. Using only two database tables and extremely simple queries, this system should work on just about any database application, event the dreaded Microsoft Access.Kinky eCards Kinky eCards is a totally free ColdFusion based eCards application. It does not require a database."+
-" Each item of eCard data is stored in an XML document that is securely encrypted. Kinky File Explorer The Kinky File Explorer is a totally free ColdFusion based file exploration system designed to provide read-only access to a specific directory of files. The idea here is that you can provide users with an easy way to view your project codebase without having to worry about them snooping around. KinkyTwits KinkyTwits is a totally free ColdFusion and jQuery powered Twitter client. It was built to be a single-user client and therefore works off of one Twitter account login. It stores its persisted data in JSON files on the server and therefore can run without the use of a database. POI Utility The POI Utility is a ColdFusion component and set of ColdFusion custom tags that helps you read and write native Microsoft Excel files. While reading Excel files is nice, the ColdFusion custom tags provide a powerful way for users to create richly formatted reports."+
-"Pusher.cfc - Realtime Notification Pusher.cfc is a ColdFusion component that facilitates posting messages to Pusher - a realtime notification service powered by HTML5 WebSockets. This component allows your application to communicate with your clients (browsers) in realtime, as needed, rather than forcing your clients to execute long-polling approaches to listen for new data. XDOM.cfc - Easy XML Traversal, Manipulation, And Merging Inspired by jQuery, XDOM.cfc is a collection-based ColdFusion component wrapper that provides an easy-to-use API for XML traversal, manipulation, and merging. Internally, XDOM maintains an aggregate of XML nodes that can be searched, deleted, and augmented with ease." ;
View
8 tests/browser/specrunner.html
@@ -16,10 +16,10 @@
<script src="corpora/5.js"></script>
- <script src="./../lib/underscore-1.2.3.js"></script>
- <script src="./../lib/underscore.string-2.0.0.js"></script>
- <script src="./../lib/stemmer.js"></script>
- <script src="./../sum.js"></script>
+ <script src="./../../lib/underscore-1.2.3.js"></script>
+ <script src="./../../lib/underscore.string-2.0.0.js"></script>
+ <script src="./../../lib/porter-stemmer.js"></script>
+ <script src="./../../sum.browser.js"></script>
<script src="specs/SpecSum.js"></script>
</head>
View
7 tests/browser/specs/SpecSum.js
@@ -24,12 +24,13 @@ describe( 'test sum\' params', function () {
expect( actual ).toBe( true );
});
});
-describe( 'summarize.js tests', function () {
+describe( 'summarize.js basic output test', function () {
corpora.forEach( function (corpus) {
it( 'should calculate the summary', function () {
var actual = sum({ 'corpus': corpus.text, 'nSentences': 3 });
- var expected = corpus.expected;
- expect(actual.summary).toEqual( expected );
+ var expected = 3;
+ expect(actual.sentences.length).toEqual( expected );
});
});
}) ;
+//TODO add tests to validate correctness of the actual output
View
2  tests/node/node.js
@@ -1,2 +0,0 @@
-var sum = require( './../sum.js' );
-var vows = require( 'vows' );
View
24 tests/node/sum.js
@@ -0,0 +1,24 @@
+var vows = require( 'vows' );
+var assert = require( 'assert' );
+
+var sum = require( './../../sum.js' );
+var corpus = "MEDICINE Testing, Testing Unusual proteins could improve cancer diagnosis and reduce deaths The key to treating cancer is to catch it early. But identifying the subtle changes in cells that betray their turncoat tendencies requires skill-and good luck-on the part of pathologists. Many cancers are not spotted until too late, when the rebel colonies are well enough established to put up a fight and found new mutinous outposts. Matritech, a Massachusetts-based start-up company, has developed a diagnostic technique that detects bladder cancer more easily-and possibly more effectively-than existing methods can. Matritech's test, which the company expects the Food and Drug Administration to approve by this summer, measures the amount of a particular type of protein in urine. Bladder cancer patients excrete this substance, called a nuclear matrix protein, in greater amounts than healthy subjects do. All cell nuclei contain matrix proteins, constituents that give the nucleus its shape and organize the chromosomes. Researchers have known of their existence since the 1970s. Their possible value has become apparent just in the past few years, however, since investigators at the Massachusetts Institute of Technology showed that some nuclear matrix proteins in cancer cells are different from those in normal cells. Others are present in elevated amounts. The unusual proteins seem to explain why the nuclei of cancer cells are often oddly shaped. The proteins escape into body fluids, where they can be identified using antibodies. Thus, the way is opened to tests for abnormal matrix proteins or, as in the case of Matritech's bladder cancer test, a normal one in unusual amounts. \"There's been all this hoopla about genetic screening, but nuclear matrix protein testing could have the biggest impact of all,\" says Lance Willsey of Harvard Medical School. Stephen D. Chubb, Matritech's chief executive, says his company's test, called NMP22, detected all cases of invasive disease in a trial with 1,000 subjects who had previously been treated for bladder cancer and were being monitored for recurrences-which are very common. Furthermore, it found about 70 percent of cases of bladder cancer that was still localized and in less need of urgent treatment. A negative result meant patients had a 90 percent chance of cancer not developing in the next three to six months-a useful predictive ability, because that is the usual interval between follow-up visits for bladder cancer patients. Those figures, Chubb notes, indicate that NMP22 could be used instead of current techniques, which involve examining cells from the bladder shed in urine or viewing the inside of the bladder with a fiber-optic device (cystoscopy). Moreover, Matritech's test is one sixth the price of cystoscopy, which is typically billed at $300, and obviates any risk of infection. Matritech is initially seeking approval for NMP22 solely to check for recurrences of bladder cancer. But Chubb is not averse to the idea that NMP22 could be used more widely to screen for the disease in people who have not previously been diagnosed. NMP22 might be the first of a series of matrix protein-based tests. Although the matrix protein that NMP22 detects is found in low levels in nuclei throughout the body, other nuclear matrix proteins are more specific. In April, Robert H. Getzenberg of the University of Pittsburgh Cancer Institute and his colleagues reported their discovery of five matrix proteins (not yet employed in any test) that occur exclusively in bladder cancer cells-thus suggesting the possibility of even more accurate diagnosis. Chubb states that Matritech has strong patent protection for all uses of nuclear matrix proteins as cancer diagnostics and that it is working on such tests for early detection of prostate, colon, cervical and breast cancer. Most of these will be based on proteins that occur exclusively in particular cancers. But Willsey wonders whether Matritech has sufficient resources to develop nuclear matrix protein-based tests as fast as the company, and he, would like. Nuclear matrix proteins could represent targets for therapeutic agents, too. The difficulty is that drugs have trouble penetrating cell nuclei. But Chubb says Matritech is giving the development of such therapeutics serious thought-and about 10 percent of its research budget.";
+
+vows
+.describe( 'testing sum.js' )
+.addBatch({
+ 'when summarizing a text': {
+ topic: function () {
+ var s = sum({
+ 'corpus': corpus,
+ 'nSentences': 3
+ });
+ return s.summary;
+ },
+ 'it should output the abstract containing the most relevant sentences for the meaning of the initial text': function (error, summary) {
+ assert.ifError( error );
+ assert.isString( summary );
+ }
+ }
+})
+.export(module);
Please sign in to comment.
Something went wrong with that request. Please try again.