Skip to content

Commit bdb6aff

Browse files
authored
Style inline images properly (#980)
1 parent d854a87 commit bdb6aff

File tree

21 files changed

+1297
-1290
lines changed

21 files changed

+1297
-1290
lines changed

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "graph-docs",
33
"private": true,
44
"version": "1.0.0",
5-
"packageManager": "pnpm@10.11.1",
5+
"packageManager": "pnpm@10.12.1",
66
"scripts": {
77
"dev": "turbo run dev",
88
"build": "NODE_OPTIONS='--max_old_space_size=8192' turbo run build",
@@ -21,7 +21,7 @@
2121
},
2222
"devDependencies": {
2323
"@edgeandnode/eslint-config": "^2.0.3",
24-
"@types/node": "^22.15.29",
24+
"@types/node": "^22.15.31",
2525
"eslint": "^8.57.1",
2626
"eslint-plugin-mdx": "^3.4.2",
2727
"prettier": "^3.5.3",

packages/og-image/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,11 @@
1212
"dependencies": {
1313
"@resvg/resvg-wasm": "^2.6.2",
1414
"react": "^18.3.1",
15-
"satori": "^0.13.2",
15+
"satori": "^0.15.2",
1616
"yoga-wasm-web": "^0.3.3"
1717
},
1818
"devDependencies": {
19-
"@cloudflare/workers-types": "^4.20250605.0",
19+
"@cloudflare/workers-types": "^4.20250610.0",
2020
"@types/react": "^18.3.23",
2121
"jest-image-snapshot": "^6.5.1",
2222
"tsx": "^4.19.4",
Lines changed: 38 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,19 @@
11
/**
2-
* @import {Element, Root} from 'hast'
2+
* @import {Root} from 'hast'
33
*/
44

5-
/**
6-
* This is a private fork of https://github.com/rehypejs/rehype-unwrap-images to work around a Nextra bug.
7-
* See https://github.com/rehypejs/rehype-unwrap-images/pull/1 for why this won't be merged upstream.
8-
*/
9-
10-
import { interactive } from 'hast-util-interactive'
115
import { whitespace } from 'hast-util-whitespace'
126
import { SKIP, visit } from 'unist-util-visit'
137

14-
const unknown = 1
15-
const containsImage = 2
16-
const containsOther = 3
8+
const isImage = (node) =>
9+
(node.type === 'element' && node.tagName === 'img') || (node.type === 'mdxJsxFlowElement' && node.name === 'img')
1710

1811
/**
19-
* Remove the wrapping paragraph for images.
12+
* This plugin does two things:
13+
* 1. Removes the `p` tag when it only contains images (and whitespace)
14+
* 2. Adds data attributes to help with image rendering:
15+
* - `data-wrapping-image` to links that contain an image
16+
* - `data-inline-image` to images that are in a paragraph with other content (text, links, etc.)
2017
*
2118
* @returns
2219
* Transform.
@@ -32,54 +29,38 @@ export default function rehypeUnwrapImages() {
3229
*/
3330
return function (tree) {
3431
visit(tree, 'element', function (node, index, parent) {
35-
if (node.tagName === 'p' && parent && typeof index === 'number' && applicable(node, false) === containsImage) {
36-
parent.children.splice(index, 1, ...node.children)
37-
return [SKIP, index]
38-
}
39-
})
40-
}
41-
}
42-
43-
/**
44-
* Check if a node can be unraveled.
45-
*
46-
* @param {Element} node
47-
* Node.
48-
* @param {boolean} inLink
49-
* Whether the node is in a link.
50-
* @returns {1 | 2 | 3}
51-
* Info.
52-
*/
53-
function applicable(node, inLink) {
54-
/** @type {1 | 2 | 3} */
55-
let image = unknown
56-
let index = -1
57-
58-
while (++index < node.children.length) {
59-
const child = node.children[index]
32+
if (node.tagName === 'p' && parent && typeof index === 'number') {
33+
// First pass: check if the paragraph contains any non-image content
34+
const hasNonImageContent = node.children.some((child) => {
35+
if (child.type === 'text') {
36+
return !whitespace(child.value)
37+
} else {
38+
return !isImage(child)
39+
}
40+
})
6041

61-
if (child.type === 'text' && whitespace(child.value)) {
62-
// Whitespace is fine.
63-
} else if (
64-
(child.type === 'element' && child.tagName === 'img') ||
65-
(child.type === 'mdxJsxFlowElement' && child.name === 'img')
66-
) {
67-
image = containsImage
68-
} else if (!inLink && interactive(child)) {
69-
// Cast as `interactive` is always `Element`.
70-
const linkResult = applicable(/** @type {Element} */ (child), true)
42+
// Second pass: add data attributes
43+
node.children.forEach((child) => {
44+
if (child.type === 'element' && child.tagName === 'a' && child.children.some(isImage)) {
45+
child.properties = child.properties || {}
46+
child.properties['data-wrapping-image'] = true
47+
} else if (isImage(child) && hasNonImageContent) {
48+
if (child.type === 'mdxJsxFlowElement') {
49+
child.attributes = child.attributes || []
50+
child.attributes.push({ type: 'mdxJsxAttribute', name: 'data-inline-image', value: true })
51+
} else {
52+
child.properties = child.properties || {}
53+
child.properties['data-inline-image'] = true
54+
}
55+
}
56+
})
7157

72-
if (linkResult === containsOther) {
73-
return containsOther
58+
// If the paragraph only contains images (and whitespace), remove it
59+
if (!hasNonImageContent) {
60+
parent.children.splice(index, 1, ...node.children)
61+
return [SKIP, index]
62+
}
7463
}
75-
76-
if (linkResult === containsImage) {
77-
image = containsImage
78-
}
79-
} else {
80-
return containsOther
81-
}
64+
})
8265
}
83-
84-
return image
8566
}

0 commit comments

Comments
 (0)