Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,16 @@ jobs:
strategy:
matrix:
node-version: [22, 24]
react-version: [18, 19]
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: ${{ matrix.node-version }}
cache: yarn
- run: yarn install --frozen-lockfile --ignore-scripts
- if: matrix.react-version != 19
run: yarn add --dev --ignore-scripts react@^${{ matrix.react-version }} react-dom@^${{ matrix.react-version }}
- run: yarn lint
- run: yarn prettier:check
- run: yarn build-and-test
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@
"license": "Apache-2.0",
"peerDependencies": {
"draft-js": ">=0.7.0",
"react": "^15.0.2 || ^16.0.0-rc || ^16.0.0 || ^17.0.0 || ^18.0.0",
"react-dom": "^15.0.2 || ^16.0.0-rc || ^16.0.0 || ^17.0.0 || ^18.0.0"
"react": "^15.0.2 || ^16.0.0-rc || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^15.0.2 || ^16.0.0-rc || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
},
"dependencies": {
"@babel/runtime": "^7.5.5",
Expand All @@ -66,8 +66,8 @@
"jest": "^30.5.2",
"jest-environment-jsdom": "^30.5.2",
"prettier": "^3.9.10",
"react": "^18.0.0",
"react-dom": "^18.0.0",
"react": "^19.3.1",
"react-dom": "^19.3.1",
"rimraf": "^6.1.3",
"rollup": "^4.66.0"
},
Expand Down
4 changes: 2 additions & 2 deletions src/convertToHTML.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// import Immutable from 'immutable'; // eslint-disable-line no-unused-vars
import invariant from 'invariant';
import React from 'react';
import ReactDOMServer from 'react-dom/server';
import renderToStaticMarkup from './util/renderToStaticMarkup';
import { convertToRaw } from 'draft-js';

import encodeBlock from './encodeBlock';
Expand Down Expand Up @@ -124,7 +124,7 @@ const convertToHTML =
Object.prototype.hasOwnProperty.call(blockHTML, 'empty')
) {
if (React.isValidElement(blockHTML.empty)) {
html = ReactDOMServer.renderToStaticMarkup(blockHTML.empty);
html = renderToStaticMarkup(blockHTML.empty);
} else {
html = blockHTML.empty;
}
Expand Down
4 changes: 2 additions & 2 deletions src/util/getBlockTags.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import invariant from 'invariant';
import React from 'react';
import ReactDOMServer from 'react-dom/server';
import renderToStaticMarkup from './renderToStaticMarkup';
import splitReactElement from './splitReactElement';

function hasChildren(element) {
Expand All @@ -22,7 +22,7 @@ export default function getBlockTags(blockHTML) {

if (React.isValidElement(blockHTML)) {
if (hasChildren(blockHTML)) {
return ReactDOMServer.renderToStaticMarkup(blockHTML);
return renderToStaticMarkup(blockHTML);
}

return splitReactElement(blockHTML);
Expand Down
4 changes: 2 additions & 2 deletions src/util/getElementHTML.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import invariant from 'invariant';
import React from 'react';
import ReactDOMServer from 'react-dom/server';
import renderToStaticMarkup from './renderToStaticMarkup';
import splitReactElement from './splitReactElement';

function hasChildren(element) {
Expand All @@ -21,7 +21,7 @@ export default function getElementHTML(element, text = null) {

if (React.isValidElement(element)) {
if (hasChildren(element)) {
return ReactDOMServer.renderToStaticMarkup(element);
return renderToStaticMarkup(element);
}

const tags = splitReactElement(element);
Expand Down
12 changes: 12 additions & 0 deletions src/util/renderToStaticMarkup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import React from 'react';
import ReactDOMServer from 'react-dom/server';

const IMAGE_PRELOADS =
/<link rel="preload" (?:as="image"|href="[^"]*" as="image")[^>]*\/>/g;

const hoistsImagePreloads = parseInt(React.version, 10) >= 19;

export default function renderToStaticMarkup(element) {
const html = ReactDOMServer.renderToStaticMarkup(element);
return hoistsImagePreloads ? html.replace(IMAGE_PRELOADS, '') : html;
}
6 changes: 3 additions & 3 deletions src/util/splitReactElement.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import invariant from 'invariant';
import React from 'react';
import ReactDOMServer from 'react-dom/server';
import renderToStaticMarkup from './renderToStaticMarkup';

// see http://w3c.github.io/html/syntax.html#writing-html-documents-elements
const VOID_TAGS = [
Expand All @@ -22,10 +22,10 @@ const VOID_TAGS = [

export default function splitReactElement(element) {
if (VOID_TAGS.indexOf(element.type) !== -1) {
return ReactDOMServer.renderToStaticMarkup(element);
return renderToStaticMarkup(element);
}

const tags = ReactDOMServer.renderToStaticMarkup(
const tags = renderToStaticMarkup(
React.cloneElement(element, {}, '\r')
).split('\r');

Expand Down
112 changes: 112 additions & 0 deletions test/spec/convertToHTML-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1099,4 +1099,116 @@ describe('convertToHTML', () => {
/missing HTML definition/
);
});

it('renders images in block HTML without preload links', () => {
const contentState = buildContentState([
{
type: 'atomic',
text: ' ',
},
]);

const result = convertToHTML({
blockToHTML: {
atomic: (
<figure>
<img src="https://x.test/a.png" />
</figure>
),
},
})(contentState);

expect(result).toBe('<figure><img src="https://x.test/a.png"/></figure>');
});

it('renders several image entities in one block without preload links', () => {
const contentState = buildContentState(
[
{
type: 'unstyled',
text: 'a b',
entityRanges: [
{
key: 0,
offset: 0,
length: 1,
},
{
key: 1,
offset: 2,
length: 1,
},
],
},
],
{
0: {
type: 'IMAGE',
mutability: 'IMMUTABLE',
data: { src: 'https://x.test/1.png' },
},
1: {
type: 'IMAGE',
mutability: 'IMMUTABLE',
data: { src: 'https://x.test/2.png' },
},
}
);

const result = convertToHTML({
entityToHTML: (entity, originalText) => {
if (entity.type === 'IMAGE') {
return <img src={entity.data.src} />;
}
return originalText;
},
})(contentState);

expect(result).toBe(
'<p><img src="https://x.test/1.png"/> <img src="https://x.test/2.png"/></p>'
);
});

it('renders an image inside an entity element without preload links', () => {
const contentState = buildContentState(
[
{
type: 'unstyled',
text: 'Hello',
entityRanges: [
{
key: 0,
offset: 0,
length: 5,
},
],
},
],
{
0: {
type: 'LINK',
mutability: 'MUTABLE',
data: { url: 'https://x.test' },
},
}
);

const result = convertToHTML({
entityToHTML: (entity, originalText) => {
if (entity.type === 'LINK') {
return (
<a href={entity.data.url}>
<img src="https://x.test/icon.png" />
{originalText}
</a>
);
}
return originalText;
},
})(contentState);

expect(result).toBe(
'<p><a href="https://x.test"><img src="https://x.test/icon.png"/>Hello</a></p>'
);
});
});
74 changes: 74 additions & 0 deletions test/spec/renderToStaticMarkup-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import React from 'react';
import renderToStaticMarkup from '../../src/util/renderToStaticMarkup';

const isReact19 = parseInt(React.version, 10) >= 19;

describe('renderToStaticMarkup', () => {
it('renders an image without a preload link', () => {
expect(renderToStaticMarkup(<img src="https://x.test/a.png" />)).toBe(
'<img src="https://x.test/a.png"/>'
);
});

it('renders a srcSet image without a preload link', () => {
expect(
renderToStaticMarkup(
<img src="https://x.test/a.png" srcSet="https://x.test/a2.png 2x" />
)
).toBe(
'<img src="https://x.test/a.png" srcSet="https://x.test/a2.png 2x"/>'
);
});

it('renders several images without preload links', () => {
expect(
renderToStaticMarkup(
<div>
<img src="https://x.test/1.png" />
<img src="https://x.test/2.png" />
</div>
)
).toBe(
'<div><img src="https://x.test/1.png"/><img src="https://x.test/2.png"/></div>'
);
});

it('keeps links that are not image preloads', () => {
expect(
renderToStaticMarkup(
<div>
<link rel="icon" href="https://x.test/i.ico" />
</div>
)
).toContain('<link rel="icon" href="https://x.test/i.ico"/>');
});

it('keeps an explicit image preload link only before React 19', () => {
const result = renderToStaticMarkup(
<div>
<link rel="preload" as="image" href="https://x.test/p.png" />
</div>
);

expect(result).toBe(
isReact19
? '<div></div>'
: '<div><link rel="preload" as="image" href="https://x.test/p.png"/></div>'
);
});

it('leaves React 19 blocking javascript: URLs in place', () => {
const consoleError = jest
.spyOn(console, 'error')
.mockImplementation(() => {});

const result = renderToStaticMarkup(<a href="javascript:alert(1)">x</a>);
consoleError.mockRestore();

expect(result).toBe(
isReact19
? '<a href="javascript:throw new Error(&#x27;React has blocked a javascript: URL as a security precaution.&#x27;)">x</a>'
: '<a href="javascript:alert(1)">x</a>'
);
});
});
33 changes: 14 additions & 19 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3690,7 +3690,7 @@ lodash@^4.17.11:
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.18.1.tgz#ff2b66c1f6326d59513de2407bf881439812771c"
integrity sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==

loose-envify@^1.0.0, loose-envify@^1.1.0:
loose-envify@^1.0.0:
version "1.3.1"
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.3.1.tgz#d1a8ad33fa9ce0e713d65fdd0ac8b748d478c848"
dependencies:
Expand Down Expand Up @@ -4027,20 +4027,17 @@ qified@^0.10.1:
dependencies:
hookified "^2.1.1"

react-dom@^18.0.0:
version "18.2.0"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d"
integrity sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==
react-dom@^19.3.1:
version "19.3.1"
resolved "https://registry.npmjs.org/react-dom/-/react-dom-19.3.1.tgz#b384c53cfcbe9c2a5d09d863b98455de9d68afe2"
integrity sha512-Y0bARz53lhEC1NMCASs0/j+faB/zqt3R8hpvg1e8+61ZVJ4UL0UdC4cBbXfZMcIuhrfgeILTLe+ZeBCavQPtKQ==
dependencies:
loose-envify "^1.1.0"
scheduler "^0.23.0"
scheduler "^0.28.0"

react@^18.0.0:
version "18.2.0"
resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5"
integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==
dependencies:
loose-envify "^1.1.0"
react@^19.3.1:
version "19.3.1"
resolved "https://registry.npmjs.org/react/-/react-19.3.1.tgz#26e9523fc36ea06be5050ebb780249a133260ad7"
integrity sha512-QNhWdt7nrgVWWQG58QuGsAR3jv3Hbe4mb4vz7w2csnHavciZJgda1CPNaldF6eVxv90ud3jUXpKlHL69gO38yw==

readdirp@~3.6.0:
version "3.6.0"
Expand Down Expand Up @@ -4171,12 +4168,10 @@ saxes@^6.0.0:
dependencies:
xmlchars "^2.2.0"

scheduler@^0.23.0:
version "0.23.0"
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.0.tgz#ba8041afc3d30eb206a487b6b384002e4e61fdfe"
integrity sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==
dependencies:
loose-envify "^1.1.0"
scheduler@^0.28.0:
version "0.28.0"
resolved "https://registry.npmjs.org/scheduler/-/scheduler-0.28.0.tgz#03d3ca348aeaf8b7502f64468f217b27523f9695"
integrity sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==

semver@^5.6.0:
version "5.7.0"
Expand Down
Loading