Skip to content
Snippets Groups Projects
Commit c265d712 authored by Damian Janus's avatar Damian Janus Committed by Bartłomiej Górnicki
Browse files

Resolve "Needed extended functionality: adding classes and target when isLinkSafeFn is false."

parent f1f7b713
No related branches found
No related tags found
1 merge request!5Resolve "Needed extended functionality: adding classes and target when isLinkSafeFn is false."
v10
\ No newline at end of file
v14.19.0
import {ClientFunction, Selector} from 'testcafe';
import { ClientFunction, Selector } from "testcafe";
fixture`Getting Started`
.page`./index.html`;
fixture`Getting Started`.page`./index.html`;
const defaultOptions = {
baseUrl: "https://hive.blog/",
breaks: true,
skipSanitization: false,
allowInsecureScriptTags: false,
addTargetBlankToLinks: true,
addNofollowToLinks: true,
addCssClassToLinks: "hive-class",
doNotShowImages: false,
ipfsPrefix: "",
assetsWidth: 640,
......@@ -24,17 +25,30 @@ const renderInBrowser = ClientFunction((options, markup) => {
return renderer.render(markup);
});
test('Renders properly simple markup', async t => {
const markup = "# H1"
test("Renders properly simple markup", async (t) => {
const markup = "# H1";
await t.click(Selector('#awaiter'))
.expect(renderInBrowser({ ...defaultOptions }, markup)).eql('<h1>H1</h1>\n');
await t
.click(Selector("#awaiter"))
.expect(renderInBrowser({ ...defaultOptions }, markup))
.eql("<h1>H1</h1>\n");
});
test('Does not crash on mixed-img markup', async t => {
test("Does not crash on mixed-img markup", async (t) => {
const markup = `<img src="![Sacrifice The Truth Logo.jpg](https://cdn.steemitimages.com/DQmUjNstssuPJpjPDDWfRnw1x2tY6AWWKcajDMGpPLA5iJf/Sacrifice%20The%20Truth%20Logo.jpg)"/>`;
const expected = `<p><img src="brokenimg.jpg" /></p>\n`;
await t.click(Selector('#awaiter'))
.expect(renderInBrowser({ ...defaultOptions }, markup)).eql(expected);
await t
.click(Selector("#awaiter"))
.expect(renderInBrowser({ ...defaultOptions }, markup))
.eql(expected);
});
test("Renders properly simple link markup with class hive-test", async (t) => {
const markup = "[Hive Link](https://hive.io)";
await t
.click(Selector("#awaiter"))
.expect(renderInBrowser({ ...defaultOptions }, markup))
.eql(`<p><a href="https://hive.io" class="hive-class">Hive Link</a></p>\n`);
});
......@@ -6,6 +6,8 @@ const renderer = new HiveContentRenderer.DefaultRenderer({
skipSanitization: false,
allowInsecureScriptTags: false,
addNofollowToLinks: true,
addTargetBlankToLink: true,
addCssClassToLinks: "hive-class",
doNotShowImages: false,
ipfsPrefix: "",
assetsWidth: 640,
......@@ -14,6 +16,7 @@ const renderer = new HiveContentRenderer.DefaultRenderer({
usertagUrlFn: (account) => "/@" + account,
hashtagUrlFn: (hashtag) => "/trending/" + hashtag,
isLinkSafeFn: (url) => true,
addCssClass: (url) => true,
});
const input = `
......@@ -22,6 +25,8 @@ const input = `
and some content
Lets mention @engrave on #hive.
[Hive Link](https://hive.io)
`;
const output = renderer.render(input);
......
......@@ -6,6 +6,8 @@ const renderer = new HiveContentRenderer.DefaultRenderer({
skipSanitization: false,
allowInsecureScriptTags: false,
addNofollowToLinks: true,
addTargetBlankToLink: true,
addCssClassToLinks: "hive-class",
doNotShowImages: false,
ipfsPrefix: "",
assetsWidth: 640,
......@@ -22,6 +24,8 @@ const input = `
and some content
Lets mention @engrave on #hive.
[Hive Link](https://hive.io)
`;
const output = renderer.render(input);
......
<!doctype html>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, height=device-height, user-scalable=no, initial-scale=1.0"/>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, height=device-height, user-scalable=no, initial-scale=1.0" />
<title>@hiveio/content-renderer live demo</title>
<style>
/* source: https://github.com/setetres/evenbettermotherfuckingwebsite */
body {margin: 5% auto; padding: 0 3rem; background: #f2f2f2; color: #444444; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; font-size: 16px; line-height: 1.8; text-shadow: 0 1px 0 #ffffff; max-width: 800px;}
code {background: white;}
a {border-bottom: 1px solid #444444; color: #444444; text-decoration: none;}
a:hover {border-bottom: 0;}
body {
margin: 5% auto;
padding: 0 3rem;
background: #f2f2f2;
color: #444444;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
font-size: 16px;
line-height: 1.8;
text-shadow: 0 1px 0 #ffffff;
max-width: 800px;
}
code {
background: white;
}
a {
border-bottom: 1px solid #444444;
color: #444444;
text-decoration: none;
}
a:hover {
border-bottom: 0;
}
/**/
h1 { font-size: 2.2em; }
h2, h3, h4, h5 { margin-bottom: 0; }
h1 {
font-size: 2.2em;
}
h2,
h3,
h4,
h5 {
margin-bottom: 0;
}
#output {
border: 1px solid #777;
padding: 0.5rem;
......@@ -48,25 +73,29 @@
<header>
<h1>@hiveio/content-renderer <small>example</small></h1>
<aside>
@hiveio/content-renderer is aimed at unifying post rendering across all Hive interfaces.
The rendering code was extracted from
<a href="https://gitlab.syncad.com/hive/condenser">condenser</a>, refactored, tested
and bundled into a standalone library. This approach allows independent development and
continous improvement of post rendering in Hive blockchain. As for now it is fully compatible
with the Hive.blog way of rendering posts. See
the <a href="https://gitlab.syncad.com/hive/hive-renderer">repository</a>, integrate into your project,
star, make pull requests and create issues. Let's make the project alive!
@hiveio/content-renderer is aimed at unifying post rendering across all Hive interfaces. The rendering
code was extracted from
<a href="https://gitlab.syncad.com/hive/condenser">condenser</a>, refactored, tested and bundled into a
standalone library. This approach allows independent development and continous improvement of post
rendering in Hive blockchain. As for now it is fully compatible with the Hive.blog way of rendering
posts. See the <a href="https://gitlab.syncad.com/hive/hive-renderer">repository</a>, integrate into
your project, star, make pull requests and create issues. Let's make the project alive!
<hr />
This example uses some markdown and transforms it to html. The library is loaded from the
unpkg CDN: <em><a href="https://unpkg.com/@hiveio/content-renderer">https://unpkg.com/@hiveio/content-renderer</a></em>.
This example uses some markdown and transforms it to html. The library is loaded from the unpkg CDN:
<em
><a href="https://unpkg.com/@hiveio/content-renderer"
>https://unpkg.com/@hiveio/content-renderer</a
></em
>.
</aside>
</header>
<h2>Render markdown:</h2>
<div class="load-post-form">
Link to post (hive.blog, peakd.com or ecency.com): <input type="text" id="post-link-input">
<button id="load-post-button">Load Hive post</button></div>
<textarea rows="12" style="width: 100%;" id="input">
Link to post (hive.blog, peakd.com or ecency.com): <input type="text" id="post-link-input" />
<button id="load-post-button">Load Hive post</button>
</div>
<textarea rows="12" style="width: 100%" id="input">
# Sample post
and some content.
......@@ -75,13 +104,13 @@ or include a tag #hive.
https://youtu.be/B7C83L6iWJQ
[Hive Link](https://hive.io)
</textarea>
<p id="render-button-container"><button id="render-button">Render markdown</button></p>
<h2>Output:</h2>
<p id="output">
...press the button...
</p>
<p id="output">...press the button...</p>
<br />
<h2>Generated HTML markup</h2>
......@@ -99,6 +128,8 @@ https://youtu.be/B7C83L6iWJQ
skipSanitization: false,
allowInsecureScriptTags: false,
addNofollowToLinks: true,
addTargetBlankToLinks: true,
addCssClassToLinks: "hive-test",
doNotShowImages: false,
ipfsPrefix: "",
assetsWidth: 640,
......@@ -135,7 +166,8 @@ https://youtu.be/B7C83L6iWJQ
let permlink = "";
if (link.length > 0) {
/* tslint:disable max-line-length */
const regex = /^\/?(?:https?:\/\/(?:hive\.blog|peakd\.com|ecency\.com))?(?:\/?[^\/\n]*\/)?@?([^\/\n]+)\/([^\/\n]+)$/giu;
const regex =
/^\/?(?:https?:\/\/(?:hive\.blog|peakd\.com|ecency\.com))?(?:\/?[^\/\n]*\/)?@?([^\/\n]+)\/([^\/\n]+)$/giu;
/* tslint:disable max-line-length */
const match = regex.exec(link);
if (match && match.length > 1) {
......@@ -164,12 +196,10 @@ https://youtu.be/B7C83L6iWJQ
console.log("Content loaded", postMarkdown);
inputElem.text(postMarkdown);
render();
}
catch (error) {
} catch (error) {
inputElem.text("Error while loading post @" + author + "/" + permlink + ": " + error);
}
})();
});
});
</script>
......
......@@ -14,6 +14,8 @@ describe("DefaultRender", () => {
breaks: true,
skipSanitization: false,
allowInsecureScriptTags: false,
addTargetBlankToLinks: true,
addCssClassToLinks: "hive-test",
addNofollowToLinks: true,
doNotShowImages: false,
ipfsPrefix: "",
......@@ -36,24 +38,23 @@ describe("DefaultRender", () => {
{
name: "Renders steem mentions correctly",
raw: "Content @noisy another content",
expected: '<p>Content <a href="https://steemit.com/@noisy">@noisy</a> another content</p>',
expected:
'<p>Content <a href="https://steemit.com/@noisy" class="hive-test">@noisy</a> another content</p>',
},
{
name: "Renders steem hashtags correctly",
raw: "Content #pl-nuda another content",
expected: '<p>Content <a href="/trending/pl-nuda">#pl-nuda</a> another content</p>',
expected: '<p>Content <a href="/trending/pl-nuda" class="hive-test">#pl-nuda</a> another content</p>',
},
{
name: "Embeds correctly vimeo video via paste",
raw:
'<iframe src="https://player.vimeo.com/video/174544848?byline=0" width="640" height="360" frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen></iframe>',
raw: '<iframe src="https://player.vimeo.com/video/174544848?byline=0" width="640" height="360" frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen></iframe>',
expected:
'<div class="videoWrapper"><iframe frameborder="0" allowfullscreen="allowfullscreen" webkitallowfullscreen="webkitallowfullscreen" mozallowfullscreen="mozallowfullscreen" src="https://player.vimeo.com/video/174544848" width="640" height="480"></iframe></div>',
},
{
name: "Embeds correctly youtube video via paste",
raw:
'<iframe width="560" height="315" src="https://www.youtube.com/embed/0nFkmd-A7jA" frameborder="0" allowfullscreen></iframe>',
raw: '<iframe width="560" height="315" src="https://www.youtube.com/embed/0nFkmd-A7jA" frameborder="0" allowfullscreen></iframe>',
expected:
'<div class="videoWrapper"><iframe width="640" height="480" src="https://www.youtube.com/embed/0nFkmd-A7jA" allowfullscreen="allowfullscreen" webkitallowfullscreen="webkitallowfullscreen" mozallowfullscreen="mozallowfullscreen" frameborder="0"></iframe></div>',
},
......@@ -71,22 +72,20 @@ describe("DefaultRender", () => {
},
{
name: "Allows links embedded via <a> tags",
raw:
"<a href='https://steemit.com/utopian-io/@blockchainstudio/drugswars-revenue-and-transaction-analysis'>Drugwars - revenue and transaction analysis</a>",
raw: '<a href="https://steemit.com/utopian-io/@blockchainstudio/drugswars-revenue-and-transaction-analysis" class="hive-test">Drugwars - revenue and transaction analysis</a>',
expected:
'<p><a href="https://steemit.com/utopian-io/@blockchainstudio/drugswars-revenue-and-transaction-analysis">Drugwars - revenue and transaction analysis</a></p>',
'<p><a href="https://steemit.com/utopian-io/@blockchainstudio/drugswars-revenue-and-transaction-analysis" class="hive-test">Drugwars - revenue and transaction analysis</a></p>',
},
{
name: "Allows links embedded via <a> tags inside of markdown headers",
raw:
"## <a href='https://steemit.com/utopian-io/@blockchainstudio/drugswars-revenue-and-transaction-analysis'>Drugwars - revenue and transaction analysis</a>",
raw: "## <a href='https://steemit.com/utopian-io/@blockchainstudio/drugswars-revenue-and-transaction-analysis' class='hive-test'>Drugwars - revenue and transaction analysis</a>",
expected:
'<h2><a href="https://steemit.com/utopian-io/@blockchainstudio/drugswars-revenue-and-transaction-analysis">Drugwars - revenue and transaction analysis</a></h2>',
'<h2><a href="https://steemit.com/utopian-io/@blockchainstudio/drugswars-revenue-and-transaction-analysis" class="hive-test">Drugwars - revenue and transaction analysis</a></h2>',
},
];
tests.forEach(test =>
tests.forEach((test) =>
it(test.name, () => {
const renderer = new DefaultRenderer(defaultOptions);
const rendered = renderer.render(test.raw).trim();
......
......@@ -28,6 +28,8 @@ export class DefaultRenderer {
iframeWidth: this.options.assetsWidth,
iframeHeight: this.options.assetsHeight,
addNofollowToLinks: this.options.addNofollowToLinks,
addTargetBlankToLinks: this.options.addTargetBlankToLinks,
addCssClassToLinks: this.options.addCssClassToLinks,
noImage: this.options.doNotShowImages,
isLinkSafeFn: this.options.isLinkSafeFn,
},
......@@ -117,6 +119,8 @@ export namespace DefaultRenderer {
skipSanitization: boolean;
allowInsecureScriptTags: boolean;
addNofollowToLinks: boolean;
addTargetBlankToLinks?: boolean;
addCssClassToLinks?: string;
doNotShowImages: boolean;
ipfsPrefix: string;
assetsWidth: number;
......@@ -133,6 +137,8 @@ export namespace DefaultRenderer {
ow(o.breaks, "Options.breaks", ow.boolean);
ow(o.skipSanitization, "Options.skipSanitization", ow.boolean);
ow(o.addNofollowToLinks, "Options.addNofollowToLinks", ow.boolean);
ow(o.addTargetBlankToLinks, "Options.addTargetBlankToLinks", ow.optional.boolean);
ow(o.addCssClassToLinks, "Options.addCssClassToLinks", ow.optional.string);
ow(o.doNotShowImages, "Options.doNotShowImages", ow.boolean);
ow(o.ipfsPrefix, "Options.ipfsPrefix", ow.string);
ow(o.assetsWidth, "Options.assetsWidth", ow.number.integer.positive);
......
......@@ -54,7 +54,7 @@ export class TagTransformingSanitizer {
img: ["src", "alt"],
// title is only set in the case of an external link warning
a: ["href", "rel", "title"],
a: ["href", "rel", "title", "class"],
},
allowedSchemes: ["http", "https", "steem"],
transformTags: {
......@@ -131,7 +131,7 @@ export class TagTransformingSanitizer {
"videoWrapper",
"phishy",
];
const validClass = classWhitelist.find(e => attribs.class === e);
const validClass = classWhitelist.find((e) => attribs.class === e);
if (validClass) {
attys.class = validClass;
}
......@@ -167,6 +167,10 @@ export class TagTransformingSanitizer {
// attys.target = '_blank' // pending iframe impl https://mathiasbynens.github.io/rel-noopener/
attys.rel = this.options.addNofollowToLinks ? "nofollow noopener" : "noopener";
attys.title = this.localization.phishingWarning;
attys.target = this.options.addTargetBlankToLinks ? "_blank" : "_self";
}
if (this.options.addCssClassToLinks) {
attys.class = this.options.addCssClassToLinks ? this.options.addCssClassToLinks : "";
}
const retTag: sanitize.Tag = {
tagName,
......@@ -184,6 +188,8 @@ export namespace TagTransformingSanitizer {
iframeWidth: number;
iframeHeight: number;
addNofollowToLinks: boolean;
addTargetBlankToLinks?: boolean;
addCssClassToLinks?: string;
noImage: boolean;
isLinkSafeFn: (url: string) => boolean;
}
......@@ -193,6 +199,8 @@ export namespace TagTransformingSanitizer {
ow(o.iframeWidth, "TagTransformingSanitizer.Options.iframeWidth", ow.number.integer.positive);
ow(o.iframeHeight, "TagTransformingSanitizer.Options.iframeHeight", ow.number.integer.positive);
ow(o.addNofollowToLinks, "TagTransformingSanitizer.Options.addNofollowToLinks", ow.boolean);
ow(o.addTargetBlankToLinks, "TagTransformingSanitizer.Options.addTargetBlankToLinks", ow.optional.boolean);
ow(o.addCssClassToLinks, "TagTransformingSanitizer.Options.addCssClassToLinks", ow.optional.string);
ow(o.noImage, "TagTransformingSanitizer.Options.noImage", ow.boolean);
ow(o.isLinkSafeFn, "TagTransformingSanitizer.Options.isLinkSafeFn", ow.function);
}
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment