mirror of
https://github.com/showdownjs/showdown.git
synced 2024-03-22 13:30:55 +08:00
Merge remote-tracking branch 'tstone/syntax-extensions'
This commit is contained in:
commit
c0d6f9fb21
100
README.md
100
README.md
|
@ -104,6 +104,34 @@ Showdown has been tested successfully with:
|
|||
In theory, Showdown will work in any browser that supports ECMA 262 3rd Edition (JavaScript 1.5). The converter itself might even work in things that aren't web browsers, like Acrobat. No promises.
|
||||
|
||||
|
||||
Extensions
|
||||
----------
|
||||
|
||||
Showdown allows additional functionality to be loaded via extensions.
|
||||
|
||||
### Client-side Extension Usage
|
||||
|
||||
```js
|
||||
<script src="src/showdown.js" />
|
||||
<script src="src/extensions/twitter.js" />
|
||||
|
||||
var converter = new Showdown().converter({ extensions: 'twitter' });
|
||||
```
|
||||
|
||||
### Server-side Extension Usage
|
||||
|
||||
```js
|
||||
// Using a bundled extension
|
||||
var Showdown = require('showdown');
|
||||
var converter = new Showdown().converter({ extensions: ['twitter'] });
|
||||
|
||||
// Using a custom extension
|
||||
var mine = require('./custom-extensions/mine');
|
||||
var converter = new Showdown().converter({ extensions: ['twitter', mine] });
|
||||
```
|
||||
|
||||
|
||||
|
||||
Known Differences in Output
|
||||
---------------------------
|
||||
|
||||
|
@ -203,6 +231,78 @@ Once installed the tests can be run from the project root using:
|
|||
New test cases can easily be added. Create a markdown file (ending in `.md`) which contains the markdown to test. Create a `.html` file of the exact same name. It will automatically be tested when the tests are executed with `mocha`.
|
||||
|
||||
|
||||
Creating Markdown Extensions
|
||||
----------------------------
|
||||
|
||||
A showdown extension is simply a function which returns an array of extensions. Each single extension can be one of two types:
|
||||
|
||||
- Language Extension -- Language extensions are ones that that add new markdown syntax to showdown. For example, say you wanted `^^youtube http://www.youtube.com/watch?v=oHg5SJYRHA0` to automatically render as an embedded YouTube video, that would be a language extension.
|
||||
- Output Modifiers -- After showdown has run, and generated HTML, an output modifier would change that HTML. For example, say you wanted to change `<div class="header">` to be `<header>`, that would be an output modifier.
|
||||
|
||||
Each extension can provide two combinations of interfaces for showdown.
|
||||
|
||||
#### Regex/Replace
|
||||
|
||||
Regex/replace style extensions are very similar to javascripts `string.replace` function. Two properties are given, `regex` and `replace`. `regex` is a string and `replace` can be either a string or a function. If `replace` is a string, it can use the `$1` syntax for group substituation, exactly as if it were making use of `string.replace` (internally it does this actually); The value of `regex` is assumed to be a global replacement.
|
||||
|
||||
#### Regex/Replace Example
|
||||
|
||||
``` js
|
||||
var demo = function(converter) {
|
||||
return [
|
||||
// Replace escaped @ symbols
|
||||
{ type: 'lang', regex: '\\@', replace: '@' }
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
#### Filter
|
||||
|
||||
Alternately, if you'd just like to do everything yourself, you can specify a filter which is a callback with a single input parameter, text (the current source text within the showdown engine).
|
||||
|
||||
#### Filter Example
|
||||
|
||||
``` js
|
||||
var demo = function(converter) {
|
||||
return [
|
||||
// Replace escaped @ symbols
|
||||
{ type: 'lang', function(text) {
|
||||
return text.replace(/\\@/g, '@');
|
||||
}}
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
#### Implementation Concerns
|
||||
|
||||
One bit which should be taken into account is maintaining both client-side and server-side compatibility. This can be achieved with a few lines of boilerplate code. First, to prevent polluting the global scope for client-side code, the extension definition should be wrapped in a self executing function.
|
||||
|
||||
``` js
|
||||
(function(){
|
||||
// Your extension here
|
||||
}());
|
||||
```
|
||||
|
||||
Second, client-side extensions should add a property onto `Showdown.extensions` which matches the name of the file. As an example, a file named `demo.js` should then add `Showdown.extensions.demo`. Server-side extensions can simply export themselves.
|
||||
|
||||
``` js
|
||||
(function(){
|
||||
var demo = function(converter) {
|
||||
// ... extension code here ...
|
||||
};
|
||||
|
||||
// Client-side export
|
||||
if (typeof window !== 'undefined' && window.Showdown && window.Showdown.extensions) { window.Showdown.extensions.demo = demo; }
|
||||
// Server-side export
|
||||
if (typeof module !== 'undefined') module.exports = demo;
|
||||
}());
|
||||
```
|
||||
|
||||
#### Testing Extensions
|
||||
|
||||
The showdown test runner is setup to automatically test cases for extensions. To add test cases for an extension, create a new folder under `./test/extensions` which matches the name of the `.js` file in `./src/extensions`. Place any test cases into the filder using the md/html format and they will automatically be run when tests are run.
|
||||
|
||||
|
||||
Credits
|
||||
---------------------------
|
||||
|
||||
|
|
|
@ -15,12 +15,16 @@
|
|||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/coreyti/showdown.git"
|
||||
"url": "https://github.com/coreyti/showdown.git",
|
||||
"web": "https://github.com/coreyti/showdown"
|
||||
},
|
||||
"devDependencies": {
|
||||
"mocha": "*",
|
||||
"should": "*"
|
||||
},
|
||||
"licenses": [{ "type": "BSD" }],
|
||||
"licenses": [{
|
||||
"type": "BSD",
|
||||
"url": "https://github.com/coreyti/showdown/raw/master/license.txt"
|
||||
}],
|
||||
"main": "./src/showdown"
|
||||
}
|
||||
|
|
30
src/extensions/google-prettify.js
Normal file
30
src/extensions/google-prettify.js
Normal file
|
@ -0,0 +1,30 @@
|
|||
|
||||
//
|
||||
// Google Prettify
|
||||
// A showdown extension to add Google Prettify (http://code.google.com/p/google-code-prettify/)
|
||||
// hints to showdown's HTML output.
|
||||
//
|
||||
|
||||
(function(){
|
||||
|
||||
var prettify = function(converter) {
|
||||
return [
|
||||
{ type: 'output', filter: function(source){
|
||||
|
||||
return source.replace(/(<pre>)?<code>/gi, function(match, pre) {
|
||||
if (pre) {
|
||||
return '<pre class="prettyprint linenums" tabIndex="0"><code data-inner="1">';
|
||||
} else {
|
||||
return '<code class="prettyprint">';
|
||||
}
|
||||
});
|
||||
}}
|
||||
];
|
||||
};
|
||||
|
||||
// Client-side export
|
||||
if (typeof window !== 'undefined' && window.Showdown && window.Showdown.extensions) { window.Showdown.extensions.googlePrettify = prettify; }
|
||||
// Server-side export
|
||||
if (typeof module !== 'undefined') module.exports = prettify;
|
||||
|
||||
}());
|
43
src/extensions/twitter.js
Normal file
43
src/extensions/twitter.js
Normal file
|
@ -0,0 +1,43 @@
|
|||
|
||||
//
|
||||
// Twitter Extension
|
||||
// @username -> <a href="http://twitter.com/username">@username</a>
|
||||
// #hashtag -> <a href="http://twitter.com/search/%23hashtag">#hashtag</a>
|
||||
//
|
||||
|
||||
(function(){
|
||||
|
||||
var twitter = function(converter) {
|
||||
return [
|
||||
|
||||
// @username syntax
|
||||
{ type: 'lang', regex: '\\B(\\\\)?@([\\S]+)\\b', replace: function(match, leadingSlash, username) {
|
||||
// Check if we matched the leading \ and return nothing changed if so
|
||||
if (leadingSlash === '\\') {
|
||||
return match;
|
||||
} else {
|
||||
return '<a href="http://twitter.com/' + username + '">@' + username + '</a>';
|
||||
}
|
||||
}},
|
||||
|
||||
// #hashtag syntax
|
||||
{ type: 'lang', regex: '\\B(\\\\)?#([\\S]+)\\b', replace: function(match, leadingSlash, tag) {
|
||||
// Check if we matched the leading \ and return nothing changed if so
|
||||
if (leadingSlash === '\\') {
|
||||
return match;
|
||||
} else {
|
||||
return '<a href="http://twitter.com/search/%23' + tag + '">#' + tag + '</a>';
|
||||
}
|
||||
}},
|
||||
|
||||
// Escaped @'s
|
||||
{ type: 'lang', regex: '\\\\@', replace: '@' }
|
||||
];
|
||||
};
|
||||
|
||||
// Client-side export
|
||||
if (typeof window !== 'undefined' && window.Showdown && window.Showdown.extensions) { window.Showdown.extensions.twitter = twitter; }
|
||||
// Server-side export
|
||||
if (typeof module !== 'undefined') module.exports = twitter;
|
||||
|
||||
}());
|
135
src/showdown.js
135
src/showdown.js
|
@ -64,7 +64,28 @@
|
|||
//
|
||||
// Showdown namespace
|
||||
//
|
||||
var Showdown = {};
|
||||
var Showdown = { extensions: {} };
|
||||
|
||||
//
|
||||
// forEach
|
||||
//
|
||||
var forEach = Showdown.forEach = function(obj, callback) {
|
||||
if (typeof obj.forEach === 'function') {
|
||||
obj.forEach(callback);
|
||||
} else {
|
||||
var i, len = obj.length;
|
||||
for (i = 0; i < len; i++) {
|
||||
callback(obj[i], i, obj);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
//
|
||||
// Standard extension naming
|
||||
//
|
||||
var stdExtName = function(s) {
|
||||
return s.replace(/[_-]||\s/g, '').toLowerCase();
|
||||
};
|
||||
|
||||
//
|
||||
// converter
|
||||
|
@ -72,7 +93,7 @@ var Showdown = {};
|
|||
// Wraps all "globals" so that the only thing
|
||||
// exposed is makeHtml().
|
||||
//
|
||||
Showdown.converter = function() {
|
||||
Showdown.converter = function(converter_options) {
|
||||
|
||||
//
|
||||
// Globals:
|
||||
|
@ -87,6 +108,68 @@ var g_html_blocks;
|
|||
// (see _ProcessListItems() for details):
|
||||
var g_list_level = 0;
|
||||
|
||||
// Global extensions
|
||||
var g_lang_extensions = [];
|
||||
var g_output_modifiers = [];
|
||||
|
||||
|
||||
//
|
||||
// Automatic Extension Loading (node only):
|
||||
//
|
||||
|
||||
if (typeof module !== 'undefind' && typeof exports !== 'undefined' && typeof require !== 'undefind') {
|
||||
var fs = require('fs');
|
||||
|
||||
if (fs) {
|
||||
// Search extensions folder
|
||||
var extensions = fs.readdirSync((__dirname || '.')+'/extensions').filter(function(file){
|
||||
return ~file.indexOf('.js');
|
||||
}).map(function(file){
|
||||
return file.replace(/\.js$/, '');
|
||||
});
|
||||
// Load extensions into Showdown namespace
|
||||
extensions.forEach(function(ext){
|
||||
var name = stdExtName(ext);
|
||||
Showdown.extensions[name] = require('./extensions/' + ext);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Options:
|
||||
//
|
||||
|
||||
// Parse extensinos options into separate arrays
|
||||
if (converter_options && converter_options.extensions) {
|
||||
|
||||
// Iterate over each plugin
|
||||
converter_options.extensions.forEach(function(plugin){
|
||||
|
||||
// Assume it's a bundled plugin if a string is given
|
||||
if (typeof plugin === 'string') {
|
||||
plugin = Showdown.extensions[stdExtName(plugin)];
|
||||
}
|
||||
|
||||
if (typeof plugin === 'function') {
|
||||
// Iterate over each extension within that plugin
|
||||
plugin(this).forEach(function(ext){
|
||||
// Sort extensions by type
|
||||
if (ext.type) {
|
||||
if (ext.type === 'language' || ext.type === 'lang') {
|
||||
g_lang_extensions.push(ext);
|
||||
} else if (ext.type === 'output' || ext.type === 'html') {
|
||||
g_output_modifiers.push(ext);
|
||||
}
|
||||
} else {
|
||||
// Assume language extension
|
||||
g_output_modifiers.push(ext);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
throw "Extension '" + plugin + "' could not be loaded. It was either not found or is not a valid extension.";
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.makeHtml = function(text) {
|
||||
//
|
||||
|
@ -100,9 +183,9 @@ this.makeHtml = function(text) {
|
|||
// from other articles when generating a page which contains more than
|
||||
// one article (e.g. an index page that shows the N most recent
|
||||
// articles):
|
||||
g_urls = new Array();
|
||||
g_titles = new Array();
|
||||
g_html_blocks = new Array();
|
||||
g_urls = {};
|
||||
g_titles = {};
|
||||
g_html_blocks = [];
|
||||
|
||||
// attacklab: Replace ~ with ~T
|
||||
// This lets us use tilde as an escape char to avoid md5 hashes
|
||||
|
@ -131,6 +214,11 @@ this.makeHtml = function(text) {
|
|||
// contorted like /[ \t]*\n+/ .
|
||||
text = text.replace(/^[ \t]+$/mg,"");
|
||||
|
||||
// Run language extensions
|
||||
g_lang_extensions.forEach(function(x){
|
||||
text = _ExecuteExtension(x, text);
|
||||
});
|
||||
|
||||
// Handle github codeblocks prior to running HashHTML so that
|
||||
// HTML contained within the codeblock gets escaped propertly
|
||||
text = _DoGithubCodeBlocks(text);
|
||||
|
@ -151,10 +239,24 @@ this.makeHtml = function(text) {
|
|||
// attacklab: Restore tildes
|
||||
text = text.replace(/~T/g,"~");
|
||||
|
||||
// Run output modifiers
|
||||
g_output_modifiers.forEach(function(x){
|
||||
text = _ExecuteExtension(x, text);
|
||||
});
|
||||
|
||||
return text;
|
||||
};
|
||||
|
||||
|
||||
var _ExecuteExtension = function(ext, text) {
|
||||
if (ext.regex) {
|
||||
var re = new RegExp(ext.regex, 'g');
|
||||
return text.replace(re, ext.replace);
|
||||
} else if (ext.filter) {
|
||||
return ext.filter(text);
|
||||
}
|
||||
};
|
||||
|
||||
var _StripLinkDefinitions = function(text) {
|
||||
//
|
||||
// Strips link definitions from text, stores the URLs and titles in
|
||||
|
@ -483,7 +585,7 @@ var _DoAnchors = function(text) {
|
|||
)
|
||||
/g,writeAnchorTag);
|
||||
*/
|
||||
text = text.replace(/(\[((?:\[[^\]]*\]|[^\[\]])*)\]\([ \t]*()<?(.*?)>?[ \t]*((['"])(.*?)\6[ \t]*)?\))/g,writeAnchorTag);
|
||||
text = text.replace(/(\[((?:\[[^\]]*\]|[^\[\]])*)\]\([ \t]*()<?(.*?(?:\(.*?\).*?)?)>?[ \t]*((['"])(.*?)\6[ \t]*)?\))/g,writeAnchorTag);
|
||||
|
||||
//
|
||||
// Last, handle reference-style shortcuts: [link text]
|
||||
|
@ -1089,7 +1191,7 @@ var _FormParagraphs = function(text) {
|
|||
text = text.replace(/\n+$/g,"");
|
||||
|
||||
var grafs = text.split(/\n{2,}/g);
|
||||
var grafsOut = new Array();
|
||||
var grafsOut = [];
|
||||
|
||||
//
|
||||
// Wrap <p> tags.
|
||||
|
@ -1208,16 +1310,9 @@ var _EncodeEmailAddress = function(addr) {
|
|||
// mailing list: <http://tinyurl.com/yu7ue>
|
||||
//
|
||||
|
||||
// attacklab: why can't javascript speak hex?
|
||||
function char2hex(ch) {
|
||||
var hexDigits = '0123456789ABCDEF';
|
||||
var dec = ch.charCodeAt(0);
|
||||
return(hexDigits.charAt(dec>>4) + hexDigits.charAt(dec&15));
|
||||
}
|
||||
|
||||
var encode = [
|
||||
function(ch){return "&#"+ch.charCodeAt(0)+";";},
|
||||
function(ch){return "&#x"+char2hex(ch)+";";},
|
||||
function(ch){return "&#x"+ch.charCodeAt(0).toString(16)+";";},
|
||||
function(ch){return ch;}
|
||||
];
|
||||
|
||||
|
@ -1337,5 +1432,15 @@ var escapeCharacters_callback = function(wholeMatch,m1) {
|
|||
|
||||
} // end of Showdown.converter
|
||||
|
||||
|
||||
// export
|
||||
if (typeof module !== 'undefined') module.exports = Showdown;
|
||||
|
||||
// stolen from AMD branch of underscore
|
||||
// AMD define happens at the end for compatibility with AMD loaders
|
||||
// that don't enforce next-turn semantics on modules.
|
||||
if (typeof define === 'function' && define.amd) {
|
||||
define('showdown', function() {
|
||||
return Showdown;
|
||||
});
|
||||
}
|
||||
|
|
2
test/cases/url-with-parenthesis.html
Normal file
2
test/cases/url-with-parenthesis.html
Normal file
|
@ -0,0 +1,2 @@
|
|||
|
||||
<p>There's an <a href="http://en.memory-alpha.org/wiki/Darmok_(episode)">episode</a> of Star Trek: The Next Generation</p>
|
2
test/cases/url-with-parenthesis.md
Normal file
2
test/cases/url-with-parenthesis.md
Normal file
|
@ -0,0 +1,2 @@
|
|||
|
||||
There's an [episode](http://en.memory-alpha.org/wiki/Darmok_(episode)) of Star Trek: The Next Generation
|
7
test/extensions/google-prettify/basic.html
Normal file
7
test/extensions/google-prettify/basic.html
Normal file
|
@ -0,0 +1,7 @@
|
|||
|
||||
<p>Here's a simple hello world in javascript:</p>
|
||||
|
||||
<pre class="prettyprint linenums" tabIndex="0"><code data-inner="1">alert('Hello World!');
|
||||
</code></pre>
|
||||
|
||||
<p>The <code class="prettyprint">alert</code> function is a build-in global from <code class="prettyprint">window</code>.</p>
|
6
test/extensions/google-prettify/basic.md
Normal file
6
test/extensions/google-prettify/basic.md
Normal file
|
@ -0,0 +1,6 @@
|
|||
|
||||
Here's a simple hello world in javascript:
|
||||
|
||||
alert('Hello World!');
|
||||
|
||||
The `alert` function is a build-in global from `window`.
|
5
test/extensions/twitter/basic.html
Normal file
5
test/extensions/twitter/basic.html
Normal file
|
@ -0,0 +1,5 @@
|
|||
<p>Testing of the twitter extension.</p>
|
||||
|
||||
<p>Ping <a href="http://twitter.com/andstuff">@andstuff</a> to find out more about <a href="http://twitter.com/search/%23extensions">#extensions</a> with showdown</p>
|
||||
|
||||
<p>And @something shouldn't render as a twitter link</p>
|
5
test/extensions/twitter/basic.md
Normal file
5
test/extensions/twitter/basic.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
Testing of the twitter extension.
|
||||
|
||||
Ping @andstuff to find out more about #extensions with showdown
|
||||
|
||||
And \@something shouldn't render as a twitter link
|
106
test/run.js
106
test/run.js
|
@ -1,44 +1,86 @@
|
|||
var showdown = new require('../src/showdown'),
|
||||
convertor = new showdown.converter(),
|
||||
fs = require('fs'),
|
||||
path = require('path'),
|
||||
should = require('should');
|
||||
|
||||
// Load test cases from disk
|
||||
var cases = fs.readdirSync('test/cases').filter(function(file){
|
||||
return ~file.indexOf('.md');
|
||||
}).map(function(file){
|
||||
return file.replace('.md', '');
|
||||
|
||||
var runTestsInDir = function(dir, converter) {
|
||||
|
||||
// Load test cases from disk
|
||||
var cases = fs.readdirSync(dir).filter(function(file){
|
||||
return ~file.indexOf('.md');
|
||||
}).map(function(file){
|
||||
return file.replace('.md', '');
|
||||
});
|
||||
|
||||
// Run each test case (markdown -> html)
|
||||
cases.forEach(function(test){
|
||||
var name = test.replace(/[-.]/g, ' ');
|
||||
it (name, function(){
|
||||
var mdpath = path.join(dir, test + '.md'),
|
||||
htmlpath = path.join(dir, test + '.html'),
|
||||
md = fs.readFileSync(mdpath, 'utf8'),
|
||||
expected = fs.readFileSync(htmlpath, 'utf8').trim(),
|
||||
actual = converter.makeHtml(md).trim();
|
||||
|
||||
// Normalize line returns
|
||||
expected = expected.replace(/\r/g, '');
|
||||
|
||||
// Ignore all leading/trailing whitespace
|
||||
expected = expected.split('\n').map(function(x){
|
||||
return x.trim();
|
||||
}).join('\n');
|
||||
actual = actual.split('\n').map(function(x){
|
||||
return x.trim();
|
||||
}).join('\n');
|
||||
|
||||
// Convert whitespace to a visible character so that it shows up on error reports
|
||||
expected = expected.replace(/ /g, '·');
|
||||
expected = expected.replace(/\n/g, '•\n');
|
||||
actual = actual.replace(/ /g, '·');
|
||||
actual = actual.replace(/\n/g, '•\n');
|
||||
|
||||
// Compare
|
||||
actual.should.equal(expected);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
//
|
||||
// :: Markdown to HTML testing ::
|
||||
//
|
||||
|
||||
describe('Markdown', function() {
|
||||
var converter = new showdown.converter();
|
||||
runTestsInDir('test/cases', converter);
|
||||
});
|
||||
|
||||
// Run each test case
|
||||
cases.forEach(function(test){
|
||||
var name = test.replace(/[-.]/g, ' ');
|
||||
it (name, function(){
|
||||
var mdpath = path.join('test/cases', test + '.md'),
|
||||
htmlpath = path.join('test/cases', test + '.html'),
|
||||
md = fs.readFileSync(mdpath, 'utf8'),
|
||||
expected = fs.readFileSync(htmlpath, 'utf8').trim(),
|
||||
actual = convertor.makeHtml(md).trim();
|
||||
|
||||
// Normalize line returns
|
||||
expected = expected.replace(/\r/g, '');
|
||||
//
|
||||
// :: Extensions Testing ::
|
||||
//
|
||||
|
||||
// Ignore all leading/trailing whitespace
|
||||
expected = expected.split('\n').map(function(x){
|
||||
return x.trim();
|
||||
}).join('\n');
|
||||
actual = actual.split('\n').map(function(x){
|
||||
return x.trim();
|
||||
}).join('\n');
|
||||
if (path.existsSync('test/extensions')) {
|
||||
|
||||
// Convert whitespace to a visible character so that it shows up on error reports
|
||||
expected = expected.replace(/ /g, '·');
|
||||
expected = expected.replace(/\n/g, '•\n');
|
||||
actual = actual.replace(/ /g, '·');
|
||||
actual = actual.replace(/\n/g, '•\n');
|
||||
describe('extensions', function() {
|
||||
// Search all sub-folders looking for directory-specific tests
|
||||
var extensions = fs.readdirSync('test/extensions').filter(function(file){
|
||||
return fs.lstatSync('test/extensions/' + file).isDirectory();
|
||||
});
|
||||
|
||||
// Compare
|
||||
actual.should.equal(expected);
|
||||
// Run tests in each extension sub-folder
|
||||
extensions.forEach(function(ext){
|
||||
// Make sure extension exists
|
||||
var src = 'src/extensions/' + ext + '.js';
|
||||
if (!path.existsSync(src)) {
|
||||
throw "Attempting tests for '" + ext + "' but sourc file (" + src + ") was not found.";
|
||||
}
|
||||
|
||||
var converter = new showdown.converter({ extensions: [ ext ] });
|
||||
var dir = 'test/extensions/' + ext;
|
||||
runTestsInDir(dir, converter);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
}
|
Loading…
Reference in New Issue
Block a user