Skip to content

Commit

Permalink
version 1.2.0: support inline Markdown-alike image declarations
Browse files Browse the repository at this point in the history
  • Loading branch information
Mithgol committed Sep 23, 2015
1 parent d8c9e82 commit c2dae50
Show file tree
Hide file tree
Showing 4 changed files with 186 additions and 27 deletions.
24 changes: 19 additions & 5 deletions README.md
Expand Up @@ -25,6 +25,8 @@ var FidoHTML = require('fidohtml');
var decoder = FidoHTML(options);
```

### Options

The `options` object (or any of its properties) may be absent. When present, the following properties are used:

* `options.dataMode` — by default it is `false`; when it's `true`, some HTML5 attributes remain unpopulated and the corresponding `data-XXXX` attributes are populated instead. (In this mode additional client-side JavaScript processing of HTML5 tags becomes necessary. Useful for whitelisting, preprocessing or otherwise preventing the default behaviour of a browser.)
Expand All @@ -44,15 +46,19 @@ The `options` object (or any of its properties) may be absent. When present,
* `options.fileURLParts` — by default it is `false`; when altered, it should be given an array of two strings that control the appearance of an URL for every uuencoded file in the message: the first string is added before the filename and the second string is added after a filename to get the complete URL of that file. (For example, when the array `['https://example.org/fidonet?area://Test/', '?time=2015']` is given, it means that the file `example.zip` has the complete URL `https://example.org/fidonet?area://Test/example.zip?time=2015`.) The default `false` value means that there's no known way to construct a file's URL from its name. (In that default case an [RFC2397-compliant](http://tools.ietf.org/html/rfc2397) Data URI of the file is created and used. Its length is usually much greater.)
* **Note:**   URLs of the files are not affected by the `URLPrefixes` option.

* `options.URLPrefixes` — by default it is `{'*': ''}`; in this object properties' names correspond to URL schemes and properties' values correspond to the prefixes that should be added to URLs (encountered in the message) when these URLs are finally converted to hyperlinks. (For example, the URL `telnet:towel.blinkenlights.nl` gets converted to a hyperlink pointing to `https://example.org/console?telnet:towel.blinkenlights.nl` if `options.URLPrefixes.telnet` is `'https://example.org/console?'`.) The value of `options.URLPrefixes['*']` is used when the value for a particular URL scheme is `undefined`. If a mere prefix is not sufficient, a function may be given that accepts an original URL and returns the transformed URL (such function should be synchronous).
* `options.URLPrefixes` — by default it is `{'*': ''}`; in this object properties' names correspond to URL schemes and properties' values correspond to the prefixes that should be added to URLs (encountered in the message) when these URLs are finally converted to hyperlinks (or to images' addresses). For example, the URL `telnet:towel.blinkenlights.nl` gets converted to a hyperlink pointing to `https://example.org/console?telnet:towel.blinkenlights.nl` if `options.URLPrefixes.telnet` is `'https://example.org/console?'`.
* If a mere prefix is not sufficient, a function may be given that accepts an original URL and returns the transformed URL (such function must be synchronous).
* The value of `options.URLPrefixes['*']` is used when the value for a particular URL scheme is `undefined`.

### Methods

The constructed object has the following methods:

### setOptions(options)
#### setOptions(options)

This method can be used to alter some (or all) of the options that were previously set in the constructor. It affects the subsequent `.fromText` calls.

### fromText(messageText)
#### fromText(messageText)

This method generates (and returns) HTML code from the given Fidonet message's text.

Expand Down Expand Up @@ -103,10 +109,18 @@ The following conversions are performed:

* [Fidonet Unicode substrings](https://github.com/Mithgol/fiunis) are converted to their Unicode equivalents (but not in UUE blocks).

* URLs become hyperlinks, i.e. each URL is wrapped in `<a>…</a>` tags.
* Inline Markdown-alike image declarations `![alt text](URL "title")` are converted to images.
* Backslashes can be used to escape literal closing square brackets (i.e. `\]` means a literal `]` character) in the alternative text to prevent a premature ending of the text.
* Backslashes can be used to escape literal quotes (i.e. `\"` means a literal `"` character) in the title to prevent a premature ending of the title.
* Titles are optional (i.e. `(URL)` can be given instead of `(URL "title")`).
* A value from `options.URLPrefixes` is used to modify an URL. (See above: “[Options](#options)”.)
* `options.dataMode === false` → the URL is copied to the image's `src` attribute.
* `options.dataMode === true` → an [RFC2397-compliant](http://tools.ietf.org/html/rfc2397) Data URI of [the tiniest GIF](http://probablyprogramming.com/2009/03/15/the-tiniest-gif-ever) appears in the image's `src` attribute and the real image's URL is copied to the image's `data-src` attribute instead of `src`. (Use JavaScript for whitelisting, preprocessing or otherwise preventing the default browser's action.)

* Standalone URLs become hyperlinks, i.e. each URL is wrapped in `<a>…</a>` tags (unless it was already processed as a part of some Markdown-alike declaration).
* `options.dataMode === false` → the URL is copied to the tag's `href` attribute.
* `options.dataMode === true``href="javascript:;"` attribute appears and the URL is copied to the tag's `data-href` attribute instead of `href`. (Use JavaScript for whitelisting, preprocessing or otherwise preventing the default browser's action.)
* A value from `options.URLPrefixes` is added before an URL. (For example, the URL `telnet:towel.blinkenlights.nl` is converted to `https://example.org/console?telnet:towel.blinkenlights.nl` if `options.URLPrefixes.telnet` is `'https://example.org/console?'`.) The value of `options.URLPrefixes['*']` is used when the prefix value for a particular URL scheme is `undefined`.
* A value from `options.URLPrefixes` is used to modify an URL. (See above: “[Options](#options)”.)

* If lines of text contain any character for [Box Drawing](http://www.unicode.org/charts/PDF/U2500.pdf) (except `U+2500`) or [Block Elements](http://www.unicode.org/charts/PDF/U2580.pdf), then a sequence of such lines is wrapped in `<code>…</code>` tags (to be rendered with some monospace font) and then also in `<div class="monospaceBlock">…</div>`. The latter is useful in CSS in the following cases:
* When the style of `.monospaceBlock > code` elements has to be different from the other `code` elements.
Expand Down
112 changes: 93 additions & 19 deletions fidohtml.js
Expand Up @@ -24,6 +24,22 @@ var defaults = {
}
};

// linkURLPrefixed = getPrefixedURL(
// _converter.options.URLPrefixes, loneURL.URLScheme, loneURL.URL
// );
var getPrefixedURL = function(convPrefixes, URLScheme, theURL){
var linkURLPrefix;
if( typeof convPrefixes[ URLScheme ] !== 'undefined' ){
linkURLPrefix = convPrefixes[URLScheme];
} else if( typeof convPrefixes[ '*' ] !== 'undefined' ){
linkURLPrefix = convPrefixes[ '*' ];
} else linkURLPrefix = '';

if( typeof linkURLPrefix === 'function' ) return linkURLPrefix(theURL);

return linkURLPrefix + theURL;
};

var FidoHTML = function(options){
if (!(this instanceof FidoHTML)) return new FidoHTML(options);

Expand Down Expand Up @@ -452,6 +468,76 @@ var FidoHTML = function(options){
].join('');
});

// convert ![alt](URL "title") to images
this.ASTree.defineSplitter(function(sourceCode){
/* jshint -W101 */
if( typeof sourceCode !== 'string' ) return sourceCode;
// TODO: add area|faqserv|fecho|freq support
return sourceCode.split(
/!\[((?:[^\]]|\\])*)\]\(((https?|ftp|fs):[^\s<>\x22\x27{}\^\[\]`]+)\s*(?:"((?:[^"]|\\")*)")?\)/
).map(function(sourceFragment, fragmentIndex, fragmentArray){
if( fragmentIndex % 5 === 0 ){ // simple fragment's index: 0, 5...
return sourceFragment;
} else if( fragmentIndex % 5 === 1 ){
// alt text's index: 1, 6, 11...
// next(+1) is the whole URL's index: 2, 7, 12...
// next(+2) is the URL scheme's index: 3, 8, 13...
// next(+3) is the title's index: 4, 9, 14...
var imageTitle = fragmentArray[ fragmentIndex + 3 ];
if( typeof imageTitle === 'undefined' ){
imageTitle = '';
} else imageTitle = imageTitle.replace(/\\"/g, '"');
return {
type: 'inlineImage',
textAlt: sourceFragment.replace(/\\]/g, ']'),
imageURL: fragmentArray[ fragmentIndex + 1 ],
URLScheme: fragmentArray[ fragmentIndex + 2 ],
imageTitle: imageTitle
};
} else return null;
}).filter(function(elem){
return elem !== null;
});
}, [
{ type: 'quote', props: [ 'quotedText' ] },
{ type: 'monospaceBlock', props: [ 'content' ] },
{ type: 'origin', props: ['preParens'] },
{ type: 'tearline', props: ['content'] },
{ type: 'tagline', props: ['content'] }
]);
this.ASTree.defineRenderer(['inlineImage'], function(inlineImage){
if( _converter.options.dataMode ){
return [
'<img src="',
'data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=',
'" data-src="',
getPrefixedURL(
_converter.options.URLPrefixes,
inlineImage.URLScheme,
inlineImage.imageURL
),
'" alt="',
inlineImage.textAlt,
'" title="',
inlineImage.imageTitle,
'">'
].join('');
}
return [
'<img src="',
getPrefixedURL(
_converter.options.URLPrefixes,
inlineImage.URLScheme,
inlineImage.imageURL
),
'" alt="',
inlineImage.textAlt,
'" title="',
inlineImage.imageTitle,
'">'
].join('');
});

// convert lone URLs to hyperlinks
this.ASTree.defineSplitter(function(sourceCode){
/* jshint -W101 */
Expand Down Expand Up @@ -481,35 +567,22 @@ var FidoHTML = function(options){
{ type: 'tagline', props: ['content'] }
]);
this.ASTree.defineRenderer(['loneURL'], function(loneURL /*, render*/){
var linkURLPrefix;
if(
typeof _converter.options.URLPrefixes[ loneURL.URLScheme ] !==
'undefined'
){
linkURLPrefix = _converter.options.URLPrefixes[loneURL.URLScheme];
} else if(
typeof _converter.options.URLPrefixes[ '*' ] !== 'undefined'
){
linkURLPrefix = _converter.options.URLPrefixes[ '*' ];
} else linkURLPrefix = '';

var linkURLPrefixed;
if( typeof linkURLPrefix === 'function' ){
linkURLPrefixed = linkURLPrefix(loneURL.URL);
} else linkURLPrefixed = linkURLPrefix + loneURL.URL;

if( _converter.options.dataMode ){
return [
'<a href="javascript:;" data-href="',
linkURLPrefixed,
getPrefixedURL(
_converter.options.URLPrefixes, loneURL.URLScheme, loneURL.URL
),
'">',
loneURL.textURL,
'</a>'
].join('');
}
return [
'<a href="',
linkURLPrefixed,
getPrefixedURL(
_converter.options.URLPrefixes, loneURL.URLScheme, loneURL.URL
),
'">',
loneURL.textURL,
'</a>'
Expand Down Expand Up @@ -544,6 +617,7 @@ var FidoHTML = function(options){
{ type: 'monospaceBlock', props: [ 'content' ] },
{ type: 'UUE', props: [ 'source' ] },
{ type: 'loneURL', props: [ 'textURL' ] },
{ type: 'inlineImage', props: [ 'textAlt', 'imageTitle' ] },
{ type: 'origin', props: ['preParens', 'addrText'] },
{ type: 'tearline', props: ['content'] },
{ type: 'tagline', props: ['content'] }
Expand Down
2 changes: 1 addition & 1 deletion package.json
@@ -1,7 +1,7 @@
{
"name": "fidohtml",
"main": "fidohtml.js",
"version": "1.1.0",
"version": "1.2.0",
"description": "Makes HTML code out of a Fidonet message.",
"keywords": ["Fidonet", "Fido", "HTML"],
"author": { "name": "Mithgol the Webmaster" },
Expand Down
75 changes: 73 additions & 2 deletions test/test.js
Expand Up @@ -28,7 +28,78 @@ describe('Fidonet HTML parser creation', function(){
});
});

describe('URL processor', function(){
describe('Inline image processor', function(){
it('http:// image is processed in the middle of a string', function(){
assert.deepEqual(
FidoHTML.fromText('foo ![bar](http://example.com "baz") quux'),
'foo <img src="http://example.com" alt="bar" title="baz"> quux'
);
});
it('https:// image with a blank title is processed', function(){
assert.deepEqual(
FidoHTML.fromText('![foo](https://example.com "") bar'),
'<img src="https://example.com" alt="foo" title=""> bar'
);
});
it('ftp:// URL with a missing title is processed', function(){
assert.deepEqual(
FidoHTML.fromText('foo ![bar](ftp://example.com/)'),
'foo <img src="ftp://example.com/" alt="bar" title="">'
);
});
it('IPFS images are directed to the default IPFS gateway', function(){
assert.deepEqual(
FidoHTMLPrefixArea.fromText([
'foo ',
'![bar](fs:/ipfs/QmWdss6Ucc7UrnovCmq355jSTTtLFs1amgb3j6Swb1sADi)',
' baz'
].join('')),
'foo <img src="http://ipfs.io/' +
'ipfs/QmWdss6Ucc7UrnovCmq355jSTTtLFs1amgb3j6Swb1sADi" ' +
'alt="bar" title=""> baz'
);
assert.deepEqual(
FidoHTMLPrefixArea.fromText([
'foo ![](',
'fs://ipfs/QmWdss6Ucc7UrnovCmq355jSTTtLFs1amgb3j6Swb1sADi',
' "bar") baz'
].join('')),
'foo <img src="http://ipfs.io/' +
'ipfs/QmWdss6Ucc7UrnovCmq355jSTTtLFs1amgb3j6Swb1sADi" ' +
'alt="" title="bar"> baz'
);
assert.deepEqual(
FidoHTMLPrefixArea.fromText([
'foo ',
'![bar](fs:ipfs/QmWdss6Ucc7UrnovCmq355jSTTtLFs1amgb3j6Swb1sADi)',
' baz'
].join('')),
'foo <img src="http://ipfs.io/' +
'ipfs/QmWdss6Ucc7UrnovCmq355jSTTtLFs1amgb3j6Swb1sADi" ' +
'alt="bar" title=""> baz'
);
});
it('dataMode works fine', function(){
assert.deepEqual(
inDataMode.fromText('foo ![bar](http://example.com "baz") quux'),
[
'foo <img src="',
'data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=',
'" data-src="http://example.com" alt="bar" title="baz"> quux'
].join('')
);
assert.deepEqual(
inDataMode.fromText('foo ![bar](ftp://example.com/)'),
[
'foo <img src="',
'data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs=',
'" data-src="ftp://example.com/" alt="bar" title="">'
].join('')
);
});
});

describe('Standalone URL processor', function(){
it('http:// URL is processed in the middle of a string', function(){
assert.deepEqual(
FidoHTML.fromText('foo http://example.com bar'),
Expand Down Expand Up @@ -100,7 +171,7 @@ describe('URL processor', function(){
'area://Ru.Blog.Mithgol</a>&gt; bar'
);
});
it('an IPFS URL is directed to the default IPFS gateway', function(){
it('IPFS URLs are directed to the default IPFS gateway', function(){
assert.deepEqual(
FidoHTMLPrefixArea.fromText(
'foo fs:/ipfs/QmWdss6Ucc7UrnovCmq355jSTTtLFs1amgb3j6Swb1sADi bar'
Expand Down

0 comments on commit c2dae50

Please sign in to comment.