Skip to content
Snippets Groups Projects
Commit 6e566413 authored by Bartłomiej Górnicki's avatar Bartłomiej Górnicki
Browse files

feat: setup project and configuration

parent 64c81e60
No related branches found
No related tags found
1 merge request!557Move hive renderer to internal packages
Showing
with 17473 additions and 1 deletion
.idea
.vscode
/dist
/.DS_Store
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
# next.js build output
.next
stages:
- test
- release
image: node:14.19.0
before_script:
- npm ci
test:
stage: test
script:
- NODE_ENV=production npm run build
- npm run test
- node sample/demo-local.js
release:
stage: release
script:
- npm run semantic-release
rules:
- if: '$CI_COMMIT_BRANCH == "master"'
/_dev
/.travis.yml
/.prettierrc.yml
/TODO
/README.md
/LICENSE
/src
/.DS_Store
/sample
/tslint.json
/tsconfig.lint.json
*.test.ts
/dist/browser/statistics.html
.gitlab-ci.yml
.nvmrc 0 → 100644
v10
\ No newline at end of file
trailingComma: all
tabWidth: 4
semi: true
printWidth: 120
singleQuote: false
LICENSE 0 → 100644
MIT License
Copyright (c) 2019 Wise Team
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
# Hive Renderer
# @hiveio/content-renderer
[![npm](https://img.shields.io/npm/v/@hiveio/content-renderer.svg?style=flat-square)](https://www.npmjs.com/package/@hiveio/content-renderer) [![License](https://img.shields.io/github/license/wise-team/steem-content-renderer.svg?style=flat-square)](https://github.com/wise-team/steem-content-renderer/blob/master/LICENSE) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](http://makeapullrequest.com)
Portable library that renders Hive posts and comments to string. It supports markdown and html and mimics the behaviour of condenser frontend.
Features:
- supports markdown and html
- sanitizes html and protects from XSS
**Credit**: this library is based on the code from condenser. It's aim is to allow other projects display Hive content the right way without porting the same code over and over.
## Server side usage
Installation:
```bash
$ npm install --save @hiveio/content-renderer
```
**Typescript:**
```typescript
import { DefaultRenderer } from "@hiveio/content-renderer";
const renderer = new DefaultRenderer({
baseUrl: "https://hive.blog/",
breaks: true,
skipSanitization: false,
allowInsecureScriptTags: false,
addNofollowToLinks: true,
doNotShowImages: false,
ipfsPrefix: "",
assetsWidth: 640,
assetsHeight: 480,
imageProxyFn: (url: string) => url,
usertagUrlFn: (account: string) => "/@" + account,
hashtagUrlFn: (hashtag: string) => "/trending/" + hashtag,
isLinkSafeFn: (url: string) => true,
});
const safeHtmlStr = renderer.render(postContent);
```
## Browser usage:
See [demo](./sample/live-demo.html).
```html
<script src="https://unpkg.com/@hiveio/content-renderer"></script>
<script>
const renderer = new HiveContentRenderer.DefaultRenderer({
baseUrl: "https://hive.blog/",
breaks: true,
skipSanitization: false,
allowInsecureScriptTags: false,
addNofollowToLinks: true,
doNotShowImages: false,
ipfsPrefix: "",
assetsWidth: 640,
assetsHeight: 480,
imageProxyFn: (url) => url,
usertagUrlFn: (account) => "/@" + account,
hashtagUrlFn: (hashtag) => "/trending/" + hashtag,
isLinkSafeFn: (url) => true,
});
$(document).ready(() => {
const renderMarkdownBtnElem = $("#render-button");
const inputElem = $("#input");
const outputElem = $("#output");
const outputMarkupElem = $("#output-markup");
renderMarkdownBtnElem.on("click", () => {
const input = inputElem.val();
const output = renderer.render(input);
console.log("Rendered", output);
outputElem.html(output);
outputMarkupElem.text(output);
});
});
</script>
</body>
</html>
```
import {ClientFunction, Selector} from 'testcafe';
fixture`Getting Started`
.page`./index.html`;
const defaultOptions = {
baseUrl: "https://hive.blog/",
breaks: true,
skipSanitization: false,
allowInsecureScriptTags: false,
addNofollowToLinks: true,
doNotShowImages: false,
ipfsPrefix: "",
assetsWidth: 640,
assetsHeight: 480,
imageProxyFn: (url) => url,
usertagUrlFn: (account) => `https://hive.blog/@${account}`,
hashtagUrlFn: (hashtag) => `/trending/${hashtag}`,
isLinkSafeFn: (url) => true, // !!url.match(/^(\/(?!\/)|https:\/\/hive.blog)/),
};
const renderInBrowser = ClientFunction((options, markup) => {
const renderer = new HiveContentRenderer.DefaultRenderer(options);
return renderer.render(markup);
});
test('Renders properly simple markup', async t => {
const markup = "# H1"
await t.click(Selector('#awaiter'))
.expect(renderInBrowser({ ...defaultOptions }, markup)).eql('<h1>H1</h1>\n');
});
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);
});
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Browser test of clean-stack</title>
<script>
module = {
exports: undefined,
};
</script>
<script src="../dist/browser/hive-content-renderer.min.js"></script>
<script>
console.log(HiveContentRenderer.DefaultRenderer);
</script>
</head>
<button id="awaiter">Awaiter</button>
<body>
</body>
</html>
This diff is collapsed.
{
"name": "@hiveio/content-renderer",
"version": "0.0.0-development",
"description": "Content renderer for Hive posts and comments. Markdown + HTML",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"unpkg": "dist/browser/hive-content-renderer.min.js",
"engines": {
"node": ">=8"
},
"publishConfig": {
"access": "public"
},
"scripts": {
"build:cleanbefore": "rm -rf dist",
"build:lint": "tslint -c tslint.json -p tsconfig.lint.json",
"build:node": "tsc",
"build:browser": "rm -rf dist/browser/ && NODE_ENV=production webpack -p --config webpack.config.js",
"build": "npm run build:cleanbefore && npm run build:node && npm run build:lint && npm run build:browser",
"prepare": "NODE_ENV=production npm run build",
"test": "find src -name '*.spec.test.ts' | TS_NODE_FILES=true TS_NODE_CACHE=false TS_NODE_COMPILER_OPTIONS='{\"module\":\"commonjs\"}' xargs mocha -r ts-node/register --require source-map-support/register",
"verify:browser": " testcafe --app-init-timeout 4000 --selector-timeout 2000 --assertion-timeout 2000 chrome browser-test/browser-test.js",
"lint-fix": "tslint --fix -c tslint.json -p tsconfig.lint.json",
"semantic-release": "semantic-release"
},
"dependencies": {
"ow": "^0.13.2",
"remarkable": "^1.7.1",
"sanitize-html": "^1.20.1",
"typescript-chained-error": "^1.3.2",
"universe-log": "^2.1.0",
"xmldom": "^0.1.27"
},
"devDependencies": {
"@commitlint/cli": "^8.1.0",
"@commitlint/config-conventional": "^8.1.0",
"@types/bluebird": "^3.5.27",
"@types/chai": "^4.2.0",
"@types/chai-as-promised": "^7.1.2",
"@types/jsdom": "^12.2.4",
"@types/lodash": "^4.14.136",
"@types/mocha": "^5.2.7",
"@types/node": "^12.7.1",
"@types/remarkable": "1.7.3",
"@types/sanitize-html": "^1.20.1",
"@types/sinon": "^7.0.13",
"@types/uuid": "^3.4.5",
"@types/xmldom": "^0.1.29",
"chai": "^4.2.0",
"chai-as-promised": "^7.1.1",
"husky": "^3.0.3",
"jsdom": "^15.1.1",
"lodash": "^4.17.15",
"mocha": "^6.2.0",
"semantic-release": "18.0.0",
"@semantic-release/gitlab": "7.0.3",
"sinon": "^7.4.1",
"testcafe": "^1.4.3",
"ts-node": "^8.3.0",
"tslint": "^5.18.0",
"typescript": "^3.5.3",
"uuid": "^3.3.2",
"webpack": "^4.39.1",
"webpack-cli": "^3.3.6",
"webpack-visualizer-plugin": "^0.1.11",
"wise-tslint-configuration": "^0.2.0"
},
"commitlint": {
"extends": [
"@commitlint/config-conventional"
],
"rules": {
"header-max-length": [
0
],
"scope-case": [
0
]
}
},
"husky": {
"hooks": {
"commit-msg": "commitlint -E HUSKY_GIT_PARAMS"
}
},
"repository": {
"type": "git",
"url": "https://gitlab.syncad.com/hive/hive-renderer.git"
},
"keywords": [
"hive",
"markdown",
"renderer",
"blockchain"
],
"author": "The Wise Team (https://wise-team.io/)",
"contributors": [
"Jędrzej Lewandowski <jedrzejblew@gmail.com> (https://jedrzej.lewandowski.doctor/)",
"Bartłomiej Górnicki <contact@engrave.dev> (https://engrave.dev)"
],
"license": "MIT",
"bugs": {
"url": "https://gitlab.syncad.com/hive/hive-renderer/-/issues"
},
"homepage": "https://engrave.dev",
"release": {
"plugins": [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
"@semantic-release/gitlab",
"@semantic-release/npm"
]
}
}
const HiveContentRenderer = require("@hiveio/content-renderer");
const renderer = new HiveContentRenderer.DefaultRenderer({
baseUrl: "https://hive.blog/",
breaks: true,
skipSanitization: false,
allowInsecureScriptTags: false,
addNofollowToLinks: true,
doNotShowImages: false,
ipfsPrefix: "",
assetsWidth: 640,
assetsHeight: 480,
imageProxyFn: (url) => url,
usertagUrlFn: (account) => "/@" + account,
hashtagUrlFn: (hashtag) => "/trending/" + hashtag,
isLinkSafeFn: (url) => true,
});
const input = `
# Sample post
and some content
Lets mention @engrave on #hive.
`;
const output = renderer.render(input);
console.log();
console.log("+-------------------------------+");
console.log("| @hiveio/content-renderer demo |");
console.log("+-------------------------------+");
console.log();
console.log(output);
console.log();
console.log();
const HiveContentRenderer = require("../dist/index");
const renderer = new HiveContentRenderer.DefaultRenderer({
baseUrl: "https://hive.blog/",
breaks: true,
skipSanitization: false,
allowInsecureScriptTags: false,
addNofollowToLinks: true,
doNotShowImages: false,
ipfsPrefix: "",
assetsWidth: 640,
assetsHeight: 480,
imageProxyFn: (url) => url,
usertagUrlFn: (account) => "/@" + account,
hashtagUrlFn: (hashtag) => "/trending/" + hashtag,
isLinkSafeFn: (url) => true,
});
const input = `
# Sample post
and some content
Lets mention @engrave on #hive.
`;
const output = renderer.render(input);
console.log();
console.log("+-------------------------------+");
console.log("| @hiveio/content-renderer demo |");
console.log("+-------------------------------+");
console.log();
console.log(output);
console.log();
console.log();
<!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"/>
<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;}
/**/
h1 { font-size: 2.2em; }
h2, h3, h4, h5 { margin-bottom: 0; }
#output {
border: 1px solid #777;
padding: 0.5rem;
}
#output-markup {
width: 100%;
padding: 0.5rem;
background: #eee;
border: 1px solid #777;
overflow-x: scroll;
}
#render-button-container {
text-align: center;
}
#render-button {
padding: 0.5rem;
font-size: 1.2em;
border-radius: 0.5rem 0.5rem;
}
.load-post-form {
text-align: right;
}
header small {
color: #999;
font-size: 50%;
}
img {
max-width: 100%;
}
</style>
</head>
<body>
<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!
<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>.
</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">
# Sample post
and some content.
Let's mention @engrave
or include a tag #hive.
https://youtu.be/B7C83L6iWJQ
</textarea>
<p id="render-button-container"><button id="render-button">Render markdown</button></p>
<h2>Output:</h2>
<p id="output">
...press the button...
</p>
<br />
<h2>Generated HTML markup</h2>
<pre id="output-markup">
...press the button...
</pre>
<script src="https://cdn.jsdelivr.net/npm/@hiveio/hive-js/dist/hive.min.js"></script>
<script src="https://unpkg.com/jquery"></script>
<script src="../dist/browser/hiveio-content-renderer.min.js"></script>
<script>
const renderer = new HiveContentRenderer.DefaultRenderer({
baseUrl: "https://hive.blog/",
breaks: true,
skipSanitization: false,
allowInsecureScriptTags: false,
addNofollowToLinks: true,
doNotShowImages: false,
ipfsPrefix: "",
assetsWidth: 640,
assetsHeight: 480,
imageProxyFn: (url) => url,
usertagUrlFn: (account) => "https://hive.blog/@" + account,
hashtagUrlFn: (hashtag) => "https://hive.blog/trending/" + hashtag,
isLinkSafeFn: (url) => true,
});
$(document).ready(() => {
const renderMarkdownBtnElem = $("#render-button");
const inputElem = $("#input");
const outputElem = $("#output");
const outputMarkupElem = $("#output-markup");
const loadPostButton = $("#load-post-button");
const postLinkInput = $("#post-link-input");
function setOutput(output) {
outputElem.html(output);
outputMarkupElem.text(output);
}
function render() {
const input = inputElem.val();
const output = renderer.render(input);
console.log("Rendered", output);
setOutput(output);
}
function getAuthorAndPermlinkFromLink(link) {
let author = "";
let permlink = "";
if (link.length > 0) {
/* tslint:disable max-line-length */
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) {
author = match[1];
permlink = match[2];
}
}
return { author, permlink };
}
renderMarkdownBtnElem.on("click", () => render());
loadPostButton.on("click", () => {
const postLink = postLinkInput.val();
const { author, permlink } = getAuthorAndPermlinkFromLink(postLink);
if (!author || author.length === 0 || !permlink || permlink.length === 0) {
inputElem.text("Author or permlink is missing...");
return;
}
inputElem.text("Loading post @" + author + "/" + permlink + " ...");
(async () => {
try {
const post = await hive.api.getContentAsync(author, permlink);
const postMarkdown = post.body;
console.log("Content loaded", postMarkdown);
inputElem.text(postMarkdown);
render();
}
catch (error) {
inputElem.text("Error while loading post @" + author + "/" + permlink + ": " + error);
}
})();
});
});
</script>
</body>
</html>
import { AbstractUniverseLog } from "universe-log";
export class Log extends AbstractUniverseLog {
public static log(): Log {
return Log.INSTANCE;
}
private static INSTANCE: Log = new Log();
private constructor() {
super({
levelEnvs: ["HIVE_CONTENT_RENDERER_LOG_LEVEL", "ENGRAVE_LOG_LEVEL"],
metadata: {
library: "@hiveio/content-renderer",
},
});
}
public initialize() {
super.init();
}
public init() {
throw new Error("Instead of #init() please call #initialize() which indirectly overrides init");
}
}
import { DefaultRenderer } from "./renderers/default/DefaultRenderer";
export { DefaultRenderer } from "./renderers/default/DefaultRenderer";
export const HiveContentRenderer = {
DefaultRenderer,
};
export default HiveContentRenderer;
// tslint:disable max-line-length quotemark
import { expect } from "chai";
import { JSDOM } from "jsdom";
import "mocha";
import { Log } from "../../Log";
import { DefaultRenderer } from "./DefaultRenderer";
describe("DefaultRender", () => {
const defaultOptions: DefaultRenderer.Options = {
baseUrl: "https://steemit.com/",
breaks: true,
skipSanitization: false,
allowInsecureScriptTags: false,
addNofollowToLinks: true,
doNotShowImages: false,
ipfsPrefix: "",
assetsWidth: 640,
assetsHeight: 480,
imageProxyFn: (url: string) => url,
usertagUrlFn: (account: string) => `https://steemit.com/@${account}`,
hashtagUrlFn: (hashtag: string) => `/trending/${hashtag}`,
isLinkSafeFn: (url: string) => true, // !!url.match(/^(\/(?!\/)|https:\/\/steemit.com)/),
};
const tests = [
{ name: "Renders H1 headers correctly", raw: `# Header H1`, expected: "<h1>Header H1</h1>" },
{ name: "Renders H4 headers correctly", raw: `#### Header H4`, expected: "<h4>Header H4</h4>" },
{
name: "Renders headers and paragraphs correctly",
raw: "# Header H1\n\nSome paragraph\n\n## Header H2\n\nAnother paragraph",
expected: "<h1>Header H1</h1>\n<p>Some paragraph</p>\n<h2>Header H2</h2>\n<p>Another paragraph</p>",
},
{
name: "Renders steem mentions correctly",
raw: "Content @noisy another content",
expected: '<p>Content <a href="https://steemit.com/@noisy">@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>',
},
{
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>',
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>',
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>',
},
{
name: "Embeds correctly youtube video via youtube.com link",
raw: "https://www.youtube.com/embed/0nFkmd-A7jA",
expected:
'<p><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></p>',
},
{
name: "Embeds correctly youtube video via youtu.be link",
raw: "https://www.youtu.be/0nFkmd-A7jA",
expected:
'<p><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></p>',
},
{
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>",
expected:
'<p><a href="https://steemit.com/utopian-io/@blockchainstudio/drugswars-revenue-and-transaction-analysis">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>",
expected:
'<h2><a href="https://steemit.com/utopian-io/@blockchainstudio/drugswars-revenue-and-transaction-analysis">Drugwars - revenue and transaction analysis</a></h2>',
},
];
tests.forEach(test =>
it(test.name, () => {
const renderer = new DefaultRenderer(defaultOptions);
const rendered = renderer.render(test.raw).trim();
const renderedNode = JSDOM.fragment(rendered);
const comparisonNode = JSDOM.fragment(test.expected);
Log.log().debug("rendered", rendered);
Log.log().debug("expected", test.expected);
expect(renderedNode.isEqualNode(comparisonNode)).to.be.equal(true);
}),
);
it("Allows insecure script tags when allowInsecureScriptTags = true", () => {
const renderer = new DefaultRenderer({ ...defaultOptions, allowInsecureScriptTags: true });
const insecureContent = '<script src="">';
renderer.render(insecureContent);
});
it("Does not allow insecure script tags when allowInsecureScriptTags = false", () => {
const renderer = new DefaultRenderer({
...defaultOptions,
skipSanitization: true,
allowInsecureScriptTags: false,
});
const insecureContent = '<script src="">';
expect(() => renderer.render(insecureContent)).to.throw(/insecure content/);
});
it("Rejects mixed image tag", () => {
const renderer = new DefaultRenderer({ ...defaultOptions });
const markup = `<img src="![img.jpg](https://img.jpg)"/>`;
const rendered = renderer.render(markup);
const expected = `<p><img src="brokenimg.jpg" /></p>\n`;
expect(rendered).to.be.equal(expected);
});
});
import ow from "ow";
import * as Remarkable from "remarkable";
import { SecurityChecker } from "../../security/SecurityChecker";
import { DefaultRendererLocalization } from "./DefaultRendererLocalization";
import { AssetEmbedder } from "./embedder/AssetEmbedder";
import { PreliminarySanitizer } from "./sanitization/PreliminarySanitizer";
import { TagTransformingSanitizer } from "./sanitization/TagTransformingSanitizer";
export class DefaultRenderer {
private options: DefaultRenderer.Options;
private tagTransformingSanitizer: TagTransformingSanitizer;
private embedder: AssetEmbedder;
public constructor(
options: DefaultRenderer.Options,
localization: DefaultRendererLocalization = DefaultRendererLocalization.DEFAULT,
) {
DefaultRenderer.Options.validate(options);
this.options = options;
DefaultRendererLocalization.validate(localization);
this.tagTransformingSanitizer = new TagTransformingSanitizer(
{
iframeWidth: this.options.assetsWidth,
iframeHeight: this.options.assetsHeight,
addNofollowToLinks: this.options.addNofollowToLinks,
noImage: this.options.doNotShowImages,
isLinkSafeFn: this.options.isLinkSafeFn,
},
localization,
);
this.embedder = new AssetEmbedder(
{
ipfsPrefix: this.options.ipfsPrefix,
width: this.options.assetsWidth,
height: this.options.assetsHeight,
hideImages: this.options.doNotShowImages,
imageProxyFn: this.options.imageProxyFn,
hashtagUrlFn: this.options.hashtagUrlFn,
usertagUrlFn: this.options.usertagUrlFn,
baseUrl: this.options.baseUrl,
},
localization,
);
}
public render(input: string): string {
ow(input, "input", ow.string.nonEmpty);
return this.doRender(input);
}
private doRender(text: string): string {
text = PreliminarySanitizer.preliminarySanitize(text);
const isHtml = this.isHtml(text);
text = isHtml ? text : this.renderMarkdown(text);
text = this.wrapRenderedTextWithHtmlIfNeeded(text);
text = this.embedder.markAssets(text);
text = this.sanitize(text);
SecurityChecker.checkSecurity(text, { allowScriptTag: this.options.allowInsecureScriptTags });
text = this.embedder.insertAssets(text);
return text;
}
private renderMarkdown(text: string): string {
const renderer = new Remarkable({
html: true, // remarkable renders first then sanitize runs...
breaks: this.options.breaks,
linkify: false, // linkify is done locally
typographer: false, // https://github.com/jonschlinkert/remarkable/issues/142#issuecomment-221546793
quotes: "“”‘’",
});
return renderer.render(text);
}
private wrapRenderedTextWithHtmlIfNeeded(renderedText: string): string {
// If content isn't wrapped with an html element at this point, add it.
if (renderedText.indexOf("<html>") !== 0) {
renderedText = "<html>" + renderedText + "</html>";
}
return renderedText;
}
private isHtml(text: string): boolean {
let html = false;
// See also ReplyEditor isHtmlTest
const m = text.match(/^<html>([\S\s]*)<\/html>$/);
if (m && m.length === 2) {
html = true;
text = m[1];
} else {
// See also ReplyEditor isHtmlTest
html = /^<p>[\S\s]*<\/p>/.test(text);
}
return html;
}
private sanitize(text: string): string {
if (this.options.skipSanitization) {
return text;
}
return this.tagTransformingSanitizer.sanitize(text);
}
}
export namespace DefaultRenderer {
export interface Options {
baseUrl: string;
breaks: boolean;
skipSanitization: boolean;
allowInsecureScriptTags: boolean;
addNofollowToLinks: boolean;
doNotShowImages: boolean;
ipfsPrefix: string;
assetsWidth: number;
assetsHeight: number;
imageProxyFn: (url: string) => string;
hashtagUrlFn: (hashtag: string) => string;
usertagUrlFn: (account: string) => string;
isLinkSafeFn: (url: string) => boolean;
}
export namespace Options {
export function validate(o: Options) {
ow(o.baseUrl, "Options.baseUrl", ow.string.nonEmpty);
ow(o.breaks, "Options.breaks", ow.boolean);
ow(o.skipSanitization, "Options.skipSanitization", ow.boolean);
ow(o.addNofollowToLinks, "Options.addNofollowToLinks", ow.boolean);
ow(o.doNotShowImages, "Options.doNotShowImages", ow.boolean);
ow(o.ipfsPrefix, "Options.ipfsPrefix", ow.string);
ow(o.assetsWidth, "Options.assetsWidth", ow.number.integer.positive);
ow(o.assetsHeight, "Options.assetsHeight", ow.number.integer.positive);
ow(o.imageProxyFn, "Options.imageProxyFn", ow.function);
ow(o.hashtagUrlFn, "Options.hashtagUrlFn", ow.function);
ow(o.usertagUrlFn, "Options.usertagUrlFn", ow.function);
ow(o.isLinkSafeFn, "TagTransformingSanitizer.Options.isLinkSafeFn", ow.function);
}
}
}
import ow from "ow";
export interface DefaultRendererLocalization {
phishingWarning: string; // "Link expanded to plain text; beware of a potential phishing attempt"
externalLink: string; // "This link will take you away from example.com"
noImage: string; // "Images not allowed"
accountNameWrongLength: string; // "Account name should be between 3 and 16 characters long."
accountNameBadActor: string; // "This account is on a bad actor list"
accountNameWrongSegment: string; // "This account name contains a bad segment"
}
export namespace DefaultRendererLocalization {
export function validate(o: DefaultRendererLocalization) {
ow(o, "DefaultRendererLocalization", ow.object);
ow(o.phishingWarning, "DefaultRendererLocalization.phishingWarningMessage", ow.string.nonEmpty);
ow(o.externalLink, "DefaultRendererLocalization.externalLink", ow.string.nonEmpty);
ow(o.noImage, "DefaultRendererLocalization.noImage", ow.string.nonEmpty);
ow(o.accountNameWrongLength, "DefaultRendererLocalization.accountNameWrongLength", ow.string.nonEmpty);
ow(o.accountNameBadActor, "DefaultRendererLocalization.accountNameBadActor", ow.string.nonEmpty);
ow(o.accountNameWrongSegment, "DefaultRendererLocalization.accountNameWrongSegment", ow.string.nonEmpty);
}
export const DEFAULT: DefaultRendererLocalization = {
phishingWarning: "Link expanded to plain text; beware of a potential phishing attempt",
externalLink: "This link will take you away from example.com",
noImage: "Images not allowed",
accountNameWrongLength: "Account name should be between 3 and 16 characters long",
accountNameBadActor: "This account is on a bad actor list",
accountNameWrongSegment: "This account name contains a bad segment",
};
}
/**
* This file is based on
* - https://github.com/steemit/condenser/blob/master/src/app/utils/SanitizeConfig.js
*/
// tslint:disable max-line-length
export class StaticConfig {
public static sanitization = {
iframeWhitelist: [
{
re: /^(https?:)?\/\/player.vimeo.com\/video\/.*/i,
fn: (src: string) => {
// <iframe src="https://player.vimeo.com/video/179213493" width="640" height="360" frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen></iframe>
if (!src) {
return null;
}
const m = src.match(/https:\/\/player\.vimeo\.com\/video\/([0-9]+)/);
if (!m || m.length !== 2) {
return null;
}
return "https://player.vimeo.com/video/" + m[1];
},
},
{
re: /^(https?:)?\/\/www.youtube.com\/embed\/.*/i,
fn: (src: string) => {
return src.replace(/\?.+$/, ""); // strip query string (yt: autoplay=1,controls=0,showinfo=0, etc)
},
},
{
re: /^https:\/\/w.soundcloud.com\/player\/.*/i,
fn: (src: string) => {
if (!src) {
return null;
}
// <iframe width="100%" height="450" scrolling="no" frameborder="no" src="https://w.soundcloud.com/player/?url=https%3A//api.soundcloud.com/tracks/257659076&amp;auto_play=false&amp;hide_related=false&amp;show_comments=true&amp;show_user=true&amp;show_reposts=false&amp;visual=true"></iframe>
const m = src.match(/url=(.+?)&/);
if (!m || m.length !== 2) {
return null;
}
return (
"https://w.soundcloud.com/player/?url=" +
m[1] +
"&auto_play=false&hide_related=false&show_comments=true" +
"&show_user=true&show_reposts=false&visual=true"
);
},
},
{
re: /^(https?:)?\/\/player.twitch.tv\/.*/i,
fn: (src: string) => {
// <iframe src="https://player.twitch.tv/?channel=ninja" frameborder="0" allowfullscreen="true" scrolling="no" height="378" width="620">
return src;
},
},
],
noImageText: "(Image not shown due to low ratings)",
allowedTags: `
div, iframe, del,
a, p, b, i, q, br, ul, li, ol, img, h1, h2, h3, h4, h5, h6, hr,
blockquote, pre, code, em, strong, center, table, thead, tbody, tr, th, td,
strike, sup, sub
`
.trim()
.split(/,\s*/),
};
}
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