Skip to content

Commit

Permalink
Accept markupSanitizer option, downcase tagName and attributeName
Browse files Browse the repository at this point in the history
Add npm 'start' script, note in readme how to run tests in browser.
Refactor tests to be more DRY (use mobiledoc creation helpers).
Refactor renderers to share more helper functions.
Refactor renderer shape to remove some conditionals (adds
`#sectionElementRendererFor` and `#markupElementRendererFor`).

fixes #49
fixes #48
  • Loading branch information
bantic committed Mar 2, 2017
1 parent a3d94c8 commit 63c187d
Show file tree
Hide file tree
Showing 11 changed files with 489 additions and 405 deletions.
29 changes: 28 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,9 +129,36 @@ var renderer = new MobiledocDOMRenderer({
var rendered = renderer.render(mobiledoc);
```

#### markupSanitizer

Use this renderer option to customize how markup attribute values are sanitized.
The renderer by default sanitizes `href` values, prefixing unsafe values with
the string `"unsafe:"`. Other attribute values are passed through unchanged. To
change this behavior, pass your own markupSanitizer function to the renderer.
If the markupSanitizer function returns a string, that value will be used when
rendering. If it returns a falsy value, the renderer's default sanitizer will be
used.

```
var renderer = new MobiledocDOMRenderer({
markupSanitizer: function({tagName, attributeName, attributeValue}) {
// This function will be called for every attribute on every markup.
// Return a sanitized attributeValue or undefined (in which case the
// default sanitizer will be used)
}
});
```

The default sanitization of href values uses an environment-appropriate url
parser if it can find one. It's unlikely, but if the renderer cannot determine
a url parser it will throw. (This can happen when running the renderer in a VM
Sandbox, like ember-cli-fastboot does.) In this case you must supply a
markupSanitizer that can handle `href` sanitization.

### Tests

* `npm test`
* To run tests via testem: `npm test`
* To run tests in the browser: `npm start` and open http://localhost:4200/tests

### Releasing

Expand Down
19 changes: 9 additions & 10 deletions lib/renderer-factory.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,20 +47,18 @@

export default class RendererFactory {
constructor({
cards,
atoms,
cardOptions,
cards=[],
atoms=[],
cardOptions={},
unknownCardHandler,
unknownAtomHandler,
markupElementRenderer,
sectionElementRenderer,
dom
markupElementRenderer={},
sectionElementRenderer={},
dom,
markupSanitizer=null
}={}) {
cards = cards || [];
validateCards(cards);
atoms = atoms || [];
validateAtoms(atoms);
cardOptions = cardOptions || {};

if (!dom) {
if (typeof window === 'undefined') {
Expand All @@ -77,7 +75,8 @@
unknownAtomHandler,
markupElementRenderer,
sectionElementRenderer,
dom
dom,
markupSanitizer
};
}

Expand Down
112 changes: 61 additions & 51 deletions lib/renderers/0-2.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,30 +9,21 @@ import {
} from '../utils/section-types';
import {
isValidSectionTagName,
isMarkupSectionElementName,
isValidMarkerType
} from '../utils/tag-names';
import {
reduceAndSanitizeAttributes,
sanitizeAttributeValue
reduceAttributes
} from '../utils/sanitization-utils';
import {
createMarkupSanitizerWithFallback,
defaultSectionElementRenderer,
defaultMarkupElementRenderer
} from '../utils/render-utils';

export const MOBILEDOC_VERSION = '0.2.0';

const IMAGE_SECTION_TAG_NAME = 'img';

function createElementFromMarkerType(dom, [tagName, attributes]=['', []]){
let element = dom.createElement(tagName);
attributes = attributes || [];

for (let i=0,l=attributes.length; i<l; i=i+2) {
let propName = attributes[i],
propValue = attributes[i+1];
element.setAttribute(propName, sanitizeAttributeValue(propName, propValue, tagName));
}
return element;
}

function validateVersion(version) {
if (version !== MOBILEDOC_VERSION) {
throw new Error(`Unexpected Mobiledoc version "${version}"`);
Expand All @@ -47,7 +38,8 @@ export default class Renderer {
unknownCardHandler,
markupElementRenderer,
sectionElementRenderer,
dom
dom,
markupSanitizer
} = options;
let {
version,
Expand All @@ -64,24 +56,21 @@ export default class Renderer {
this.cards = cards;
this.cardOptions = cardOptions;
this.unknownCardHandler = unknownCardHandler || this._defaultUnknownCardHandler;
this.markupSanitizer = createMarkupSanitizerWithFallback(markupSanitizer);

this.sectionElementRenderer = {};
if (sectionElementRenderer) {
for (let key in sectionElementRenderer) {
if (sectionElementRenderer.hasOwnProperty(key)) {
this.sectionElementRenderer[key.toLowerCase()] = sectionElementRenderer[key];
}
}
}
this.sectionElementRenderer = {
'__default__': defaultSectionElementRenderer
};
Object.keys(sectionElementRenderer).forEach(key => {
this.sectionElementRenderer[key.toLowerCase()] = sectionElementRenderer[key];
});

this.markupElementRenderer = {};
if (markupElementRenderer) {
for (let key in markupElementRenderer) {
if (markupElementRenderer.hasOwnProperty(key)) {
this.markupElementRenderer[key.toLowerCase()] = markupElementRenderer[key];
}
}
}
this.markupElementRenderer = {
'__default__': defaultMarkupElementRenderer
};
Object.keys(markupElementRenderer).forEach(key => {
this.markupElementRenderer[key.toLowerCase()] = markupElementRenderer[key];
});

this._renderCallbacks = [];
this._teardownCallbacks = [];
Expand Down Expand Up @@ -160,15 +149,7 @@ export default class Renderer {
let markerType = this.markerTypes[openTypes[j]];
let [tagName, attrs=[]] = markerType;
if (isValidMarkerType(tagName)) {
let lowerCaseTagName = tagName.toLowerCase();
if (this.markupElementRenderer[lowerCaseTagName]) {
let attrObj = reduceAndSanitizeAttributes(attrs, lowerCaseTagName);
let openedElement = this.markupElementRenderer[lowerCaseTagName](tagName, this.dom, attrObj);
pushElement(openedElement);
} else {
let openedElement = createElementFromMarkerType(this.dom, markerType);
pushElement(openedElement);
}
pushElement(this.renderMarkupElement(tagName, attrs));
} else {
closeCount--;
}
Expand All @@ -183,6 +164,37 @@ export default class Renderer {
}
}

/**
* @param attrs Array
*/
renderMarkupElement(tagName, attrs) {
tagName = tagName.toLowerCase();
attrs = this.sanitizeAttributes(tagName, reduceAttributes(attrs));

let renderer = this.markupElementRendererFor(tagName);
return renderer(tagName, this.dom, attrs);
}

markupElementRendererFor(tagName) {
return this.markupElementRenderer[tagName] ||
this.markupElementRenderer.__default__;
}

sanitizeAttributes(tagName, attrsObj) {
let sanitized = {};

Object.keys(attrsObj).forEach(attributeName => {
let attributeValue = attrsObj[attributeName];
sanitized[attributeName] = this.sanitizeAttribute({tagName, attributeName, attributeValue});
});

return sanitized;
}

sanitizeAttribute({tagName, attributeName, attributeValue}) {
return this.markupSanitizer({tagName, attributeName, attributeValue});
}

renderListItem(markers) {
const element = this.dom.createElement('li');
this.renderMarkersOnElement(element, markers);
Expand Down Expand Up @@ -270,23 +282,21 @@ export default class Renderer {
}

renderMarkupSection([type, tagName, markers]) {
tagName = tagName.toLowerCase();
if (!isValidSectionTagName(tagName, MARKUP_SECTION_TYPE)) {
return;
}

let element;
let lowerCaseTagName = tagName.toLowerCase();
if (this.sectionElementRenderer[lowerCaseTagName]) {
element = this.sectionElementRenderer[lowerCaseTagName](tagName, this.dom);
} else if (isMarkupSectionElementName(tagName)) {
element = this.dom.createElement(tagName);
} else {
element = this.dom.createElement('div');
element.setAttribute('class', tagName);
}
let renderer = this.sectionElementRendererFor(tagName);
let element = renderer(tagName, this.dom);

this.renderMarkersOnElement(element, markers);
return element;
}

sectionElementRendererFor(tagName) {
return this.sectionElementRenderer[tagName] ||
this.sectionElementRenderer.__default__;
}
}

Loading

0 comments on commit 63c187d

Please sign in to comment.