` with a blue cross :
+
+```javascript
+// ... your props
+ul: (htmlAttribs, children, convertedCSSStyles, passProps) => {
+ return (
+ +
+ );
+}
+```
+
## Styling
In addition to your custom renderers, you can apply specific styles to HTML tags (`tagsStyles`) or HTML classes (`classesStyles`). You can also combine these styles with your custom renderers.
@@ -121,6 +146,8 @@ Styling options override thesmelves, so you might render a custom HTML tag with
The default style of your custom renderer will be merged to the one from your `classesStyles` which will also be merged by the `style` attribute.
+> **IMPORTANT NOTE : Do NOT use the `StyleSheet` API to create the styles you're going to feed to `tagsStyle` and `classesStyles`. Although it might look like it's working at first, the caching logic of `react-native` makes it impossible for this module to deep check each of your style to properly apply the precedence and priorities of your nested tags' styles.**
+
Here's an usage example
```javascript
@@ -130,7 +157,7 @@ Here's an usage example
const html = `
Here, we have a style set on the "i" tag with the "tagsStyles" prop.
- Finally, this paragraph is style through the classesStyles prop
`;
+ Finally, this paragraph is styled through the classesStyles prop
`;
```
![](https://puu.sh/xF7Jx/e4b395975d.png)
@@ -145,6 +172,8 @@ A nice trick, demonstrated in the [basic usage of this module](#basic-usage) is
Please note that if you set width AND height through any mean of styling, `imagesMaxWidth` will be ignored.
+Before their dimensions have been properly retrieved, images will temporarily be rendered in 100px wide squares. You can override this default value with prop `imagesInitialDimensions`.
+
Images with broken links will render an empty square with a thin border, similar to what safari renders in a webview.
Please note that all of these behaviours are implemented in the default `
` renderer. If you want to provide your own `
` renderer, you'll have to make this happen by yourself. You can use the `img` function in `HTMLRenderers.js` as a starting point.
@@ -204,7 +233,7 @@ You can't expect native components to be able to render *everything* you can fin
* `ignoredStyles` : array of ignored CSS rules. Nothing is ignored by default
* `ignoreNodesFunction` : this is a cumbersome, yet powerful, way of ignoring very specific stuff.
-**Please note** that if you supply `ignoredTags`, you will override the default ignored ones. There are *a lot* of them, if you want to keep them and add you own, you can do something like :
+**Please note** that if you supply `ignoredTags`, you will override the default ignored ones. There are *a lot* of them, if you want to keep them and add you own, you can do something like :
```javascript
import { IGNORED_TAGS } from 'react-native-render-html/HTMLUtils';
diff --git a/package.json b/package.json
index 6cf732701..cb55b0dbb 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "react-native-render-html",
- "version": "3.4.0",
+ "version": "3.5.0",
"author": "Archriss",
"license": "BSD-2-Clause",
"repository": "https://github.com/archriss/react-native-render-html",
@@ -17,6 +17,7 @@
"events": "^1.1.0",
"html-entities": "^1.2.0",
"htmlparser2": "^3.9.0",
+ "lodash.isequal": "4.5.0",
"stream": "0.0.2"
},
"peerDependencies": {
diff --git a/src/HTML.js b/src/HTML.js
index 7bd7007b8..47f4660fc 100644
--- a/src/HTML.js
+++ b/src/HTML.js
@@ -1,19 +1,21 @@
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
-import { View, Text, ViewPropTypes } from 'react-native';
-import { BLOCK_TAGS, TEXT_TAGS, IGNORED_TAGS, STYLESETS } from './HTMLUtils';
-import { cssStringToRNStyle, _getElementClassStyles } from './HTMLStyles';
+import { View, Text, ViewPropTypes, ActivityIndicator } from 'react-native';
+import { BLOCK_TAGS, TEXT_TAGS, MIXED_TAGS, IGNORED_TAGS, TEXT_TAGS_IGNORING_ASSOCIATION, STYLESETS, TextOnlyPropTypes } from './HTMLUtils';
+import { cssStringToRNStyle, _getElementClassStyles, cssStringToObject, cssObjectToString } from './HTMLStyles';
import { generateDefaultBlockStyles, generateDefaultTextStyles } from './HTMLDefaultStyles';
import htmlparser2 from 'htmlparser2';
+import _isEqual from 'lodash.isequal';
import * as HTMLRenderers from './HTMLRenderers';
export default class HTML extends PureComponent {
-
static propTypes = {
renderers: PropTypes.object.isRequired,
ignoredTags: PropTypes.array.isRequired,
ignoredStyles: PropTypes.array.isRequired,
decodeEntities: PropTypes.bool.isRequired,
+ debug: PropTypes.bool.isRequired,
+ listsPrefixesRenderers: PropTypes.object,
ignoreNodesFunction: PropTypes.func,
alterData: PropTypes.func,
alterChildren: PropTypes.func,
@@ -24,18 +26,24 @@ export default class HTML extends PureComponent {
containerStyle: ViewPropTypes ? ViewPropTypes.style : View.propTypes.style,
customWrapper: PropTypes.func,
onLinkPress: PropTypes.func,
+ onParsed: PropTypes.func,
imagesMaxWidth: PropTypes.number,
+ imagesInitialDimensions: PropTypes.shape({
+ width: PropTypes.number,
+ height: PropTypes.number
+ }),
emSize: PropTypes.number.isRequired,
- baseFontSize: PropTypes.number.isRequired
+ baseFontStyle: PropTypes.object.isRequired
}
static defaultProps = {
renderers: HTMLRenderers,
+ debug: false,
decodeEntities: true,
emSize: 14,
- baseFontSize: 14,
ignoredTags: IGNORED_TAGS,
ignoredStyles: [],
+ baseFontStyle: { fontSize: 14 },
tagsStyles: {},
classesStyles: {}
}
@@ -51,39 +59,50 @@ export default class HTML extends PureComponent {
}
componentWillMount () {
- this.registerIgnoredTags();
- this.registerDOM();
this.generateDefaultStyles();
}
+ componentDidMount () {
+ this.registerDOM();
+ }
+
componentWillReceiveProps (nextProps) {
- const { html, uri, ignoredTags, renderers, baseFontSize } = this.props;
+ const { html, uri, renderers, baseFontStyle } = this.props;
if (html !== nextProps.html || uri !== nextProps.uri) {
this.imgsToRender = [];
this.registerDOM(nextProps);
}
- if (ignoredTags !== nextProps.ignoredTags) {
- this.registerIgnoredTags(nextProps);
- }
if (renderers !== nextProps.renderers) {
this.renderers = { ...HTMLRenderers, ...(nextProps.renderers || {}) };
}
- if (baseFontSize !== nextProps.baseFontSize) {
- this.generateDefaultStyles(nextProps.baseFontSize);
+ if (!_isEqual(baseFontStyle, nextProps.baseFontStyle)) {
+ this.generateDefaultStyles(nextProps.baseFontStyle);
+ }
+ }
+
+ componentDidUpdate (prevProps, prevState) {
+ if (this.state.dom !== prevState.dom) {
+ this.parseDOM(this.state.dom);
}
}
async registerDOM (props = this.props) {
const { html, uri } = props;
if (html) {
- this.setState({ dom: props.html });
+ this.setState({ dom: html, loadingRemoteURL: false, errorLoadingRemoteURL: false });
} else if (props.uri) {
try {
// WIP : This should render a loader and html prop should not be set in state
// Error handling would be nice, too.
- let response = await fetch(uri);
- this.setState({ dom: response._bodyText });
+ try {
+ this.setState({ loadingRemoteURL: true, errorLoadingRemoteURL: false });
+ let response = await fetch(uri);
+ this.setState({ dom: response._bodyText, loadingRemoteURL: false });
+ } catch (err) {
+ console.warn(err);
+ this.setState({ errorLoadingRemoteURL: true, loadingRemoteURL: false });
+ }
} catch (err) {
console.warn('react-native-render-html', `Couldn't fetch remote HTML from uri : ${uri}`);
return false;
@@ -93,24 +112,55 @@ export default class HTML extends PureComponent {
}
}
- generateDefaultStyles (baseFontSize = this.props.baseFontSize) {
- this.defaultBlockStyles = generateDefaultBlockStyles(baseFontSize);
- this.defaultTextStyles = generateDefaultTextStyles(baseFontSize);
+ parseDOM (dom) {
+ const { decodeEntities, debug, onParsed } = this.props;
+ const parser = new htmlparser2.Parser(
+ new htmlparser2.DomHandler((_err, dom) => {
+ const RNElements = this.mapDOMNodesTORNElements(dom);
+ onParsed && onParsed(dom, RNElements);
+ this.setState({ RNNodes: this.renderRNElements(RNElements) });
+ if (debug) {
+ console.log('DOMNodes from htmlparser2', dom);
+ console.log('RNElements from render-html', RNElements);
+ }
+ }),
+ { decodeEntities: decodeEntities }
+ );
+ parser.write(dom);
+ parser.done();
}
- registerIgnoredTags (props = this.props) {
- this._ignoredTags = props.ignoredTags.map((tag) => tag.toLowerCase());
+ generateDefaultStyles (baseFontStyle = this.props.baseFontStyle) {
+ this.defaultBlockStyles = generateDefaultBlockStyles(baseFontStyle.fontSize || 14);
+ this.defaultTextStyles = generateDefaultTextStyles(baseFontStyle.fontSize || 14);
}
- shouldApplyBaseFontSize (parent, classStyles) {
- const { tagsStyles } = this.props;
- const notOverridenByStyleAttribute =
- !parent || !parent.attribs || !parent.attribs.style || (parent.attribs.style.search('font-size') === -1);
- const notOverridenByTagsStyles =
- !parent || !parent.name || !tagsStyles[parent.name] || !tagsStyles[parent.name]['fontSize'];
- const notOverrideByClassesStyle = !classStyles || !classStyles['fontSize'];
+ filterBaseFontStyles (element, classStyles) {
+ const { tagsStyles, baseFontStyle } = this.props;
+ const { tagName, parentTag, parent, attribs } = element;
+ const styles = Object.keys(baseFontStyle);
+ let appliedStyles = {};
+
+ for (let i = 0; i < styles.length; i++) {
+ const styleAttribute = styles[i];
+ const styleAttributeWithCSSDashes = styleAttribute.replace(/[A-Z]/, (match) => { return `-${match.toLowerCase()}`; });
+ const overridenFromStyle = attribs && attribs.style && attribs.style.search(styleAttributeWithCSSDashes) !== -1;
+ const overridenFromParentStyle = parent && parent.attribs && parent.attribs.style && parent.attribs.style.search(styleAttributeWithCSSDashes) !== -1;
+
+ const overridenFromTagStyle = tagName && tagsStyles[tagName] && tagsStyles[tagName][styleAttribute];
+ const overridenFromParentTagStyle = parentTag && tagsStyles[parentTag] && tagsStyles[parentTag][styleAttribute];
- return notOverridenByStyleAttribute && notOverridenByTagsStyles && notOverrideByClassesStyle;
+ const overridenFromClassStyles = classStyles && classStyles[styleAttribute];
+
+ const notOverriden = !overridenFromStyle && !overridenFromParentStyle &&
+ !overridenFromTagStyle && !overridenFromParentTagStyle &&
+ !overridenFromClassStyles;
+
+ if (notOverriden) {
+ appliedStyles[styleAttribute] = baseFontStyle[styleAttribute];
+ }
+ }
+ return appliedStyles;
}
/**
@@ -130,6 +180,15 @@ export default class HTML extends PureComponent {
return false;
}
+ wrapperHasTextChild (children) {
+ for (let i = 0; i < children.length; i++) {
+ if (children[i].wrapper === 'Text') {
+ return true;
+ }
+ }
+ return false;
+ }
+
/**
* Loops on children an find texts that need to be wrapped so we don't render line breaks
* The wrapper can either be a when it should be a paragraph, or a custom tag named
@@ -141,14 +200,14 @@ export default class HTML extends PureComponent {
associateRawTexts (children) {
for (let i = 0; i < children.length; i++) {
const child = children[i];
- if ((child.wrapper === 'Text' && child.tagName !== 'p') && children.length > 1 && (!child.parent || child.parent.name !== 'p')) {
+ if ((child.wrapper === 'Text' && TEXT_TAGS_IGNORING_ASSOCIATION.indexOf(child.tagName) === -1) && children.length > 1 && (!child.parent || child.parent.name !== 'p')) {
// Texts outside
or not
themselves (with siblings)
let wrappedTexts = [];
for (let j = i; j < children.length; j++) {
// Loop on its next siblings and store them in an array
// until we encounter a block or a
let nextSibling = children[j];
- if (nextSibling.wrapper !== 'Text' || nextSibling.tagName === 'p') {
+ if (nextSibling.wrapper !== 'Text' || TEXT_TAGS_IGNORING_ASSOCIATION.indexOf(nextSibling.tagName) !== -1) {
break;
}
wrappedTexts.push(nextSibling);
@@ -183,14 +242,14 @@ export default class HTML extends PureComponent {
* @memberof HTML
*/
mapDOMNodesTORNElements (DOMNodes, parentTag = false) {
- const { ignoreNodesFunction, alterData, alterChildren } = this.props;
+ const { ignoreNodesFunction, ignoredTags, alterData, alterChildren, tagsStyles, classesStyles } = this.props;
let RNElements = DOMNodes.map((node, nodeIndex) => {
const { type, attribs, name, parent } = node;
let { children, data } = node;
if (ignoreNodesFunction && ignoreNodesFunction(node, parentTag) === true) {
return false;
}
- if (this._ignoredTags.indexOf(node.name) !== -1) {
+ if (ignoredTags.map((tag) => tag.toLowerCase()).indexOf(node.name && node.name.toLowerCase()) !== -1) {
return false;
}
if (alterData && data) {
@@ -212,7 +271,8 @@ export default class HTML extends PureComponent {
return {
wrapper: 'Text',
data: data.replace(/(\r\n|\n|\r)/gm, ''), // remove linebreaks
- attribs, parent,
+ attribs,
+ parent,
tagName: name || 'rawtext'
};
}
@@ -225,7 +285,7 @@ export default class HTML extends PureComponent {
// If children cannot be nested in a Text, or if the tag
// maps to a block element, use a view
return { wrapper: 'View', children, attribs, parent, tagName: name, parentTag };
- } else if (TEXT_TAGS.indexOf(name.toLowerCase()) !== -1) {
+ } else if (TEXT_TAGS.indexOf(name.toLowerCase()) !== -1 || MIXED_TAGS.indexOf(name.toLowerCase()) !== -1) {
// We are able to nest its children inside a Text
return { wrapper: 'Text', children, attribs, parent, tagName: name, parentTag };
}
@@ -238,7 +298,7 @@ export default class HTML extends PureComponent {
const firstChild = children && children[0];
if (firstChild && children.length === 1) {
// Specific tweaks for wrappers with a single child
- if (attribs === firstChild.attribs &&
+ if ((attribs === firstChild.attribs || !firstChild.attribs) &&
firstChild.wrapper === wrapper &&
(tagName === firstChild.tagName || firstChild.tagName === 'rawtext')) {
// If the only child of a node is using the same wrapper, merge them into one
@@ -250,14 +310,55 @@ export default class HTML extends PureComponent {
tagName,
nodeIndex
};
- } else if (['rawtext', 'textwrapper'].indexOf(firstChild.tagName) !== -1 && wrapper === 'View') {
- // If the only child of a View node, assign its attributes to it so the
- // text styles are applied properly even when they're not the direct target
- firstChild.attribs = attribs;
- parsedNode.attribs = {};
}
}
return { ...parsedNode, nodeIndex };
+ })
+ .map((parsedNode, nodeIndex) => {
+ const { wrapper, attribs, tagName, children } = parsedNode;
+ if (wrapper === 'View' && attribs && this.wrapperHasTextChild(children)) {
+ // When encountering a View wrapper that has some styles and also Text children,
+ // let's filter out text-only styles and apply those to *all* Text children and
+ // remove them from the wrapper, mimicking browsers' behaviour better.
+ const wrapperStyles = {
+ ...(tagsStyles[tagName] || {}),
+ ...(_getElementClassStyles(attribs, classesStyles)),
+ ...cssStringToObject(attribs.style || '')
+ };
+
+ let textChildrenInheritedStyles = {};
+ Object.keys(wrapperStyles).forEach((styleKey) => {
+ // Extract text-only styles
+ if (TextOnlyPropTypes[styleKey]) {
+ textChildrenInheritedStyles[styleKey] = wrapperStyles[styleKey];
+ delete wrapperStyles[styleKey];
+ }
+ });
+ if (Object.keys(textChildrenInheritedStyles).length === 0) {
+ // No style to apply to text children, avoid unecessary loops
+ return parsedNode;
+ }
+ // Re-write wrapper's styles as a string
+ parsedNode.attribs.style = cssObjectToString(wrapperStyles);
+ for (let i = 0; i < children.length; i++) {
+ const child = children[i];
+ const { wrapper, attribs } = child;
+
+ if (wrapper === 'Text') {
+ // Set (or merge) the inherited text styles extracted from the wrapper for
+ // each Text child
+ if (!attribs.style) {
+ child.attribs.style = cssObjectToString(textChildrenInheritedStyles);
+ } else {
+ child.attribs.style = cssObjectToString({
+ ...textChildrenInheritedStyles,
+ ...cssStringToObject(child.attribs.style)
+ });
+ }
+ }
+ }
+ }
+ return parsedNode;
});
return this.associateRawTexts(RNElements);
}
@@ -273,11 +374,11 @@ export default class HTML extends PureComponent {
* @memberof HTML
*/
renderRNElements (RNElements, parentWrapper = 'root', parentIndex = 0) {
- const { tagsStyles, classesStyles, onLinkPress, imagesMaxWidth, emSize, ignoredStyles, baseFontSize } = this.props;
+ const { tagsStyles, classesStyles, emSize, ignoredStyles } = this.props;
return RNElements && RNElements.length ? RNElements.map((element, index) => {
- const { attribs, data, tagName, parent, parentTag, children, nodeIndex, wrapper } = element;
+ const { attribs, data, tagName, parentTag, children, nodeIndex, wrapper } = element;
const Wrapper = wrapper === 'Text' ? Text : View;
- const key = `${wrapper}-${parentIndex}-${nodeIndex}-${index}`;
+ const key = `${wrapper}-${parentIndex}-${nodeIndex}-${tagName}-${index}-${parentTag}`;
const convertedCSSStyles =
attribs && attribs.style ?
cssStringToRNStyle(
@@ -298,31 +399,26 @@ export default class HTML extends PureComponent {
childElements,
convertedCSSStyles,
{
+ ...this.props,
parentWrapper: wrapper,
- tagsStyles,
- classesStyles,
- onLinkPress,
- imagesMaxWidth,
parentTag,
nodeIndex,
- emSize,
- baseFontSize,
+ parentIndex,
key,
+ data,
rawChildren: children
});
}
const classStyles = _getElementClassStyles(attribs, classesStyles);
- // Base fontSize should be applied only if nothing else overrides it
- const applyBaseFontSize = this.shouldApplyBaseFontSize(parent, classStyles);
const textElement = data ?
- { data } :
+ { data } :
false;
const style = [
- (Wrapper === Text ? this.defaultTextStyles : this.defaultBlockStyles)[tagName],
- classStyles,
+ (!tagsStyles || !tagsStyles[tagName]) ? (Wrapper === Text ? this.defaultTextStyles : this.defaultBlockStyles)[tagName] : undefined,
tagsStyles ? tagsStyles[tagName] : undefined,
+ classStyles,
convertedCSSStyles
]
.filter((s) => s !== undefined);
@@ -337,23 +433,27 @@ export default class HTML extends PureComponent {
}
render () {
- const { decodeEntities, customWrapper } = this.props;
- const { dom } = this.state;
- if (!dom) {
+ const { customWrapper, remoteLoadingView, remoteErrorView } = this.props;
+ const { RNNodes, loadingRemoteURL, errorLoadingRemoteURL } = this.state;
+ if (!RNNodes && !loadingRemoteURL) {
return false;
+ } else if (loadingRemoteURL) {
+ return remoteLoadingView ?
+ remoteLoadingView(this.props, this.state) :
+ (
+
+
+
+ );
+ } else if (errorLoadingRemoteURL) {
+ return remoteErrorView ?
+ remoteErrorView(this.props, this.state) :
+ (
+
+ Could not load { this.props.uri }
+
+ );
}
- let RNNodes;
- const parser = new htmlparser2.Parser(
- new htmlparser2.DomHandler((_err, dom) => {
- // console.log('DOMNodes', dom);
- // console.log('Parsed nodes', this.mapDOMNodesTORNElements(dom));
- const RNElements = this.mapDOMNodesTORNElements(dom);
- RNNodes = this.renderRNElements(RNElements);
- }),
- { decodeEntities: decodeEntities }
- );
- parser.write(dom);
- parser.done();
return customWrapper ? customWrapper(RNNodes) : (
diff --git a/src/HTMLDefaultStyles.js b/src/HTMLDefaultStyles.js
index 33521d8dc..504f8b891 100644
--- a/src/HTMLDefaultStyles.js
+++ b/src/HTMLDefaultStyles.js
@@ -1,9 +1,7 @@
-import { StyleSheet } from 'react-native';
-
const BASE_FONT_SIZE = 14;
export function generateDefaultBlockStyles (baseFontSize = BASE_FONT_SIZE) {
- return StyleSheet.create({
+ return {
div: { },
ul: {
paddingLeft: 40,
@@ -14,7 +12,6 @@ export function generateDefaultBlockStyles (baseFontSize = BASE_FONT_SIZE) {
marginBottom: baseFontSize
},
iframe: {
- width: 200,
height: 200
},
hr: {
@@ -23,11 +20,11 @@ export function generateDefaultBlockStyles (baseFontSize = BASE_FONT_SIZE) {
height: 1,
backgroundColor: '#CCC'
}
- });
+ };
}
export function generateDefaultTextStyles (baseFontSize = BASE_FONT_SIZE) {
- return StyleSheet.create({
+ return {
u: { textDecorationLine: 'underline' },
em: { fontStyle: 'italic' },
i: { fontStyle: 'italic' },
@@ -59,7 +56,7 @@ export function generateDefaultTextStyles (baseFontSize = BASE_FONT_SIZE) {
marginTop: baseFontSize,
marginBottom: baseFontSize
}
- });
+ };
}
/**
diff --git a/src/HTMLImage.js b/src/HTMLImage.js
index 747731a1a..a788ac08a 100644
--- a/src/HTMLImage.js
+++ b/src/HTMLImage.js
@@ -1,23 +1,34 @@
import React, { PureComponent } from 'react';
-import { Image, View } from 'react-native';
+import { Image, View, Text } from 'react-native';
import PropTypes from 'prop-types';
-const DEFAULT_WIDTH = 100;
-const DEFAULT_HEIGHT = 100;
-
export default class HTMLImage extends PureComponent {
constructor (props) {
super(props);
this.state = {
- width: DEFAULT_WIDTH,
- height: DEFAULT_HEIGHT
+ width: props.imagesInitialDimensions.width,
+ height: props.imagesInitialDimensions.height
};
}
static propTypes = {
source: PropTypes.object.isRequired,
+ alt: PropTypes.string,
+ height: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
+ width: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
style: Image.propTypes.style,
- imagesMaxWidth: PropTypes.number
+ imagesMaxWidth: PropTypes.number,
+ imagesInitialDimensions: PropTypes.shape({
+ width: PropTypes.number,
+ height: PropTypes.number
+ })
+ }
+
+ static defaultProps = {
+ imagesInitialDimensions: {
+ width: 100,
+ height: 100
+ }
}
componentDidMount () {
@@ -28,36 +39,47 @@ export default class HTMLImage extends PureComponent {
this.getImageSize(nextProps);
}
- getDimensionsFromStyle (style) {
- let width;
- let height;
+ getDimensionsFromStyle (style, height, width) {
+ let styleWidth;
+ let styleHeight;
+
+ if (height) {
+ styleHeight = height;
+ }
+ if (width) {
+ styleWidth = width;
+ }
style.forEach((styles) => {
- if (styles['width']) {
- width = styles['width'];
+ if (!width && styles['width']) {
+ styleWidth = styles['width'];
}
- if (styles['height']) {
- height = styles['height'];
+ if (!height && styles['height']) {
+ styleHeight = styles['height'];
}
});
- return { width, height };
+ return { styleWidth, styleHeight };
}
getImageSize (props = this.props) {
- const { source, imagesMaxWidth, style } = props;
- const { width, height } = this.getDimensionsFromStyle(style);
+ const { source, imagesMaxWidth, style, height, width } = props;
+ const { styleWidth, styleHeight } = this.getDimensionsFromStyle(style, height, width);
- if (width && height) {
- return this.setState({ width, height });
+ if (styleWidth && styleHeight) {
+ return this.setState({
+ width: typeof styleWidth === 'string' && styleWidth.search('%') !== -1 ? styleWidth : parseInt(styleWidth, 10),
+ height: typeof styleHeight === 'string' && styleHeight.search('%') !== -1 ? styleHeight : parseInt(styleHeight, 10)
+ });
}
// Fetch image dimensions only if they aren't supplied or if with or height is missing
Image.getSize(
source.uri,
- (width, height) => {
- this.setState({
- width: imagesMaxWidth && width > imagesMaxWidth ? imagesMaxWidth : width,
- height: imagesMaxWidth && width > imagesMaxWidth ? height / (width / imagesMaxWidth) : height,
- error: false
- });
+ (originalWidth, originalHeight) => {
+ if (!imagesMaxWidth) {
+ return this.setState({ width: originalWidth, height: originalHeight });
+ }
+ const optimalWidth = imagesMaxWidth <= originalWidth ? imagesMaxWidth : originalWidth;
+ const optimalHeight = (optimalWidth * originalHeight) / originalWidth;
+ this.setState({ width: optimalWidth, height: optimalHeight, error: false });
},
() => {
this.setState({ error: true });
@@ -77,7 +99,9 @@ export default class HTMLImage extends PureComponent {
get errorImage () {
return (
-
+
+ { this.props.alt ? { this.props.alt } : false }
+
);
}
diff --git a/src/HTMLRenderers.js b/src/HTMLRenderers.js
index d2029f198..cc6c07141 100644
--- a/src/HTMLRenderers.js
+++ b/src/HTMLRenderers.js
@@ -1,10 +1,10 @@
import React from 'react';
-import { TouchableOpacity, Text, View, WebView } from 'react-native';
+import { TouchableOpacity, Text, View, WebView, Dimensions } from 'react-native';
import { _constructStyles } from './HTMLStyles';
import HTMLImage from './HTMLImage';
export function a (htmlAttribs, children, convertedCSSStyles, passProps) {
- const { parentWrapper, onLinkPress, key } = passProps;
+ const { parentWrapper, onLinkPress, key, data } = passProps;
const style = _constructStyles({
tagName: 'a',
htmlAttribs,
@@ -19,20 +19,21 @@ export function a (htmlAttribs, children, convertedCSSStyles, passProps) {
if (parentWrapper === 'Text') {
return (
- { children }
+ { children || data }
);
} else {
return (
- { children }
+ { children || data }
);
}
}
export function img (htmlAttribs, children, convertedCSSStyles, passProps = {}) {
- if (!htmlAttribs.src) {
+ const { src, alt, width, height } = htmlAttribs;
+ if (!src) {
return false;
}
@@ -43,18 +44,36 @@ export function img (htmlAttribs, children, convertedCSSStyles, passProps = {})
styleSet: 'IMAGE'
});
return (
-
+
);
}
export function ul (htmlAttribs, children, convertedCSSStyles, passProps = {}) {
- const { rawChildren, nodeIndex, key, baseFontSize } = passProps;
+ const { rawChildren, nodeIndex, key, baseFontStyle, listsPrefixesRenderers } = passProps;
+ const baseFontSize = baseFontStyle.fontSize || 14;
children = children && children.map((child, index) => {
const rawChild = rawChildren[index];
let prefix = false;
+ const rendererArgs = [
+ htmlAttribs,
+ children,
+ convertedCSSStyles,
+ {
+ ...passProps,
+ index
+ }
+ ];
+
if (rawChild) {
if (rawChild.parentTag === 'ul') {
- prefix = (
+ prefix = listsPrefixesRenderers && listsPrefixesRenderers.ul ? listsPrefixesRenderers.ul(...rendererArgs) : (
);
} else if (rawChild.parentTag === 'ol') {
- prefix = (
+ prefix = listsPrefixesRenderers && listsPrefixesRenderers.ol ? listsPrefixesRenderers.ol(...rendererArgs) : (
{ index + 1 })
);
}
}
return (
-
+
{ prefix }
{ child }
@@ -90,19 +109,28 @@ export function iframe (htmlAttribs, children, convertedCSSStyles, passProps) {
return false;
}
+ const viewportWidth = Dimensions.get('window').width;
const style = _constructStyles({
tagName: 'iframe',
htmlAttribs,
passProps,
styleSet: 'VIEW',
additionalStyles: [
- htmlAttribs.height ? { height: parseInt(htmlAttribs.height, 10) } : {},
- htmlAttribs.width ? { width: parseInt(htmlAttribs.width, 10) } : {}
+ {
+ height: htmlAttribs.height ?
+ parseInt(htmlAttribs.height, 10) :
+ undefined
+ },
+ {
+ width: htmlAttribs.width && htmlAttribs.width <= viewportWidth ?
+ parseInt(htmlAttribs.width, 10) :
+ viewportWidth
+ }
]
});
return (
-
+
);
}
@@ -112,8 +140,8 @@ export function br (htlmAttribs, children, convertedCSSStyles, passProps) {
);
}
-export function textwrapper (htmlAttribs, children, convertedCSSStyles) {
+export function textwrapper (htmlAttribs, children, convertedCSSStyles, { key }) {
return (
- { children }
+ { children }
);
}
diff --git a/src/HTMLStyles.js b/src/HTMLStyles.js
index 4be339646..f800702eb 100644
--- a/src/HTMLStyles.js
+++ b/src/HTMLStyles.js
@@ -7,7 +7,7 @@ import checkPropTypes from './checkPropTypes';
* @param str: the style string
* @return the style as an obect
*/
-function cssStringToObject (str) {
+export function cssStringToObject (str) {
return str
.split(';')
.map((prop) => prop.split(':'))
@@ -19,6 +19,14 @@ function cssStringToObject (str) {
}, {});
}
+export function cssObjectToString (obj) {
+ let string = '';
+ Object.keys(obj).forEach((style) => {
+ string += `${style}:${obj[style]};`;
+ });
+ return string;
+}
+
/**
* Helper that composes styles with the default style for a tag, the "style" attribute and
* any given addiitional style. Checks everything against the style sets of views, images,
@@ -30,9 +38,9 @@ function cssStringToObject (str) {
export function _constructStyles ({ tagName, htmlAttribs, passProps, additionalStyles, styleSet = 'VIEW', baseFontSize }) {
let defaultTextStyles = generateDefaultTextStyles(baseFontSize);
let defaultBlockStyles = generateDefaultBlockStyles(baseFontSize);
- return [
+ let style = [
(styleSet === 'VIEW' ? defaultBlockStyles : defaultTextStyles)[tagName],
- passProps.htmlStyles ? passProps.htmlStyles[tagName] : undefined,
+ passProps.tagsStyles ? passProps.tagsStyles[tagName] : undefined,
_getElementClassStyles(htmlAttribs, passProps.classesStyles),
htmlAttribs.style ?
cssStringToRNStyle(
@@ -40,10 +48,14 @@ export function _constructStyles ({ tagName, htmlAttribs, passProps, additionalS
STYLESETS[styleSet],
{ parentTag: tagName }
) :
- undefined,
- additionalStyles || undefined
- ]
- .filter((style) => style !== undefined);
+ undefined
+ ];
+
+ if (additionalStyles) {
+ style = style.concat(!additionalStyles.length ? [additionalStyles] : additionalStyles);
+ }
+
+ return style.filter((style) => style !== undefined);
}
/**
diff --git a/src/HTMLUtils.js b/src/HTMLUtils.js
index 9eda11119..633f056b6 100644
--- a/src/HTMLUtils.js
+++ b/src/HTMLUtils.js
@@ -1,19 +1,35 @@
-import _RNTextStylePropTypes from 'react-native/Libraries/Text/TextStylePropTypes';
-import _RNViewStylePropTypes from 'react-native/Libraries/Components/View/ViewStylePropTypes';
-import _RNImageStylePropTypes from 'react-native/Libraries/Image/ImageStylePropTypes';
+import TextStylesPropTypes from 'react-native/Libraries/Text/TextStylePropTypes';
+import ViewStylesPropTypes from 'react-native/Libraries/Components/View/ViewStylePropTypes';
+import ImageStylesPropTypes from 'react-native/Libraries/Image/ImageStylePropTypes';
-export const BLOCK_TAGS = ['address', 'article', 'aside', 'footer', 'hgroup', 'nav', 'section', 'blockquote', 'dd', 'div',
-'dl', 'dt', 'figure', 'hr', 'li', 'main', 'ol', 'ul', 'cite', 'data', 'rp', 'rtc', 'ruby', 'area',
-'img', 'map', 'center'];
+// Filter prop-types that are only applicable to and not
+export let TextOnlyPropTypes = {};
+Object.keys(TextStylesPropTypes).forEach((prop) => {
+ if (!ViewStylesPropTypes[prop]) {
+ TextOnlyPropTypes[prop] = TextStylesPropTypes[prop];
+ }
+});
-export const TEXT_TAGS = ['a', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'figcaption', 'p', 'pre', 'abbr', 'b', 'bdi', 'bdo', 'code',
-'dfn', 'i', 'kbd', 'mark', 'q', 'rt', 's', 'samp', 'small', 'big', 'span', 'strong', 'sub', 'sup', 'time', 'u', 'var', 'wbr',
-'del', 'ins', 'blink', 'font', 'em', 'bold', 'br'];
+// These tags should ALWAYS be mapped to View wrappers
+export const BLOCK_TAGS = ['address', 'article', 'aside', 'footer', 'hgroup', 'nav', 'section', 'blockquote', 'dd',
+ 'dl', 'dt', 'figure', 'hr', 'li', 'main', 'ol', 'ul', 'cite', 'data', 'rp', 'rtc', 'ruby', 'area',
+ 'img', 'map', 'center'];
+
+// These tags should ALWAYS be mapped to Text wrappers
+export const TEXT_TAGS = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'figcaption', 'p', 'pre', 'abbr', 'b', 'bdi', 'bdo', 'code',
+ 'dfn', 'i', 'kbd', 'mark', 'q', 'rt', 's', 'samp', 'small', 'big', 'span', 'strong', 'sub', 'sup', 'time', 'u', 'var', 'wbr',
+ 'del', 'ins', 'blink', 'font', 'em', 'bold', 'br'];
+
+// These tags can either be mapped to View or Text wrappers, depending solely on their children
+export const MIXED_TAGS = ['a'];
+
+// These text tags shouldn't be associated with their siblings in the associateRawTags method
+export const TEXT_TAGS_IGNORING_ASSOCIATION = ['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'];
export const IGNORED_TAGS = ['head', 'scripts', 'audio', 'video', 'track', 'embed', 'object', 'param', 'source', 'canvas', 'noscript',
-'caption', 'col', 'colgroup', 'table', 'tbody', 'td', 'tfoot', 'th', 'thead', 'tr', 'button', 'datalist', 'fieldset', 'form',
-'input', 'label', 'legend', 'meter', 'optgroup', 'option', 'output', 'progress', 'select', 'textarea', 'details', 'diaglog',
-'menu', 'menuitem', 'summary'];
+ 'caption', 'col', 'colgroup', 'table', 'tbody', 'td', 'tfoot', 'th', 'thead', 'tr', 'button', 'datalist', 'fieldset', 'form',
+ 'input', 'label', 'legend', 'meter', 'optgroup', 'option', 'output', 'progress', 'select', 'textarea', 'details', 'diaglog',
+ 'menu', 'menuitem', 'summary'];
// As of react-native 0.48, this might change in the future
export const PERC_SUPPORTED_STYLES = [
@@ -24,12 +40,12 @@ export const PERC_SUPPORTED_STYLES = [
];
// We have to do some munging here as the objects are wrapped
-const RNTextStylePropTypes = Object.keys(_RNTextStylePropTypes)
- .reduce((acc, k) => { acc[k] = _RNTextStylePropTypes[k]; return acc; }, {});
-const RNViewStylePropTypes = Object.keys(_RNViewStylePropTypes)
- .reduce((acc, k) => { acc[k] = _RNViewStylePropTypes[k]; return acc; }, {});
-const RNImageStylePropTypes = Object.keys(_RNImageStylePropTypes)
- .reduce((acc, k) => { acc[k] = _RNImageStylePropTypes[k]; return acc; }, {});
+const RNTextStylePropTypes = Object.keys(TextStylesPropTypes)
+ .reduce((acc, k) => { acc[k] = TextStylesPropTypes[k]; return acc; }, {});
+const RNViewStylePropTypes = Object.keys(ViewStylesPropTypes)
+ .reduce((acc, k) => { acc[k] = ViewStylesPropTypes[k]; return acc; }, {});
+const RNImageStylePropTypes = Object.keys(ImageStylesPropTypes)
+ .reduce((acc, k) => { acc[k] = ImageStylesPropTypes[k]; return acc; }, {});
export const STYLESETS = Object.freeze({ VIEW: 'view', TEXT: 'text', IMAGE: 'image' });