satteri-figure 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +64 -0
- package/index.js +149 -0
- package/package.json +33 -0
package/README.md
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# satteri-figure
|
|
2
|
+
|
|
3
|
+
[Satteri](https://satteri.bruits.org) plugin to transform an image with alt text to a figure with caption. A port of [`@microflash/rehype-figure`](https://github.com/naiyerasif/rehype-figure) to Satteri's [HAST plugin API](https://satteri.bruits.org/docs/plugin-api/).
|
|
4
|
+
|
|
5
|
+
> [!IMPORTANT]
|
|
6
|
+
> Converting an image with alt text to a figure with caption is an [escape hatch](https://en.wiktionary.org/wiki/escape_hatch). Alt text, title, and captions have [different intended purposes](https://www.stylemanual.gov.au/content-types/images/alt-text-captions-and-titles-images), and you should eventually enhance your content to adopt them.
|
|
7
|
+
|
|
8
|
+
## What's this?
|
|
9
|
+
|
|
10
|
+
This package is a [Satteri](https://satteri.bruits.org) plugin that takes an image node with alt text (e.g., ``) and converts it to a [figure](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/figure) element with caption.
|
|
11
|
+
|
|
12
|
+
```html
|
|
13
|
+
<figure>
|
|
14
|
+
<img src="path-to-image.jpg" alt="Alt text" />
|
|
15
|
+
<figcaption>Alt text</figcaption>
|
|
16
|
+
</figure>
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Install
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
npm install satteri-figure
|
|
23
|
+
yarn add satteri-figure
|
|
24
|
+
pnpm add satteri-figure
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Use
|
|
28
|
+
|
|
29
|
+
Say we have the following module `example.js`:
|
|
30
|
+
|
|
31
|
+
```js
|
|
32
|
+
import { markdownToHtml } from "satteri";
|
|
33
|
+
import satteriFigure from "satteri-figure";
|
|
34
|
+
|
|
35
|
+
const { html } = markdownToHtml("", {
|
|
36
|
+
hastPlugins: [satteriFigure()],
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
console.log(html);
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Running that with `node example.js` yields:
|
|
43
|
+
|
|
44
|
+
```html
|
|
45
|
+
<figure><img src="path-to-image.jpg" alt="Alt text"><figcaption>Alt text</figcaption></figure>
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## API
|
|
49
|
+
|
|
50
|
+
The default export is `satteriFigure`, a function returning a HAST plugin definition to pass to `hastPlugins`.
|
|
51
|
+
|
|
52
|
+
The following options are available. All of them are optional.
|
|
53
|
+
|
|
54
|
+
- `className`: class (or list of classes) for the wrapped `figure` element
|
|
55
|
+
|
|
56
|
+
By default, no classes are added to the `figure` element.
|
|
57
|
+
|
|
58
|
+
## Development
|
|
59
|
+
|
|
60
|
+
- Run `pnpm test` to run tests
|
|
61
|
+
|
|
62
|
+
## License
|
|
63
|
+
|
|
64
|
+
[MIT](../../LICENSE)
|
package/index.js
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { defineHastPlugin } from "satteri";
|
|
2
|
+
|
|
3
|
+
// HTML inter-element whitespace.
|
|
4
|
+
// See <https://infra.spec.whatwg.org/#ascii-whitespace>.
|
|
5
|
+
const whitespaceRe = /^[ \t\n\f\r]*$/;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @typedef {object} SatteriFigureOptions
|
|
9
|
+
* @property {string | string[]} [className]
|
|
10
|
+
* Class(es) for the wrapping `figure` element. No classes are added
|
|
11
|
+
* by default.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Satteri plugin to transform an image with alt text to a figure with
|
|
16
|
+
* caption.
|
|
17
|
+
*
|
|
18
|
+
* Port of [`@microflash/rehype-figure`](https://github.com/naiyerasif/rehype-figure)
|
|
19
|
+
* to a Satteri HAST plugin.
|
|
20
|
+
*
|
|
21
|
+
* @param {SatteriFigureOptions} [options]
|
|
22
|
+
* Optional settings.
|
|
23
|
+
* @returns
|
|
24
|
+
* HAST plugin definition; pass the result to `hastPlugins`.
|
|
25
|
+
*/
|
|
26
|
+
export default function satteriFigure(options = {}) {
|
|
27
|
+
return defineHastPlugin({
|
|
28
|
+
name: "satteri-figure",
|
|
29
|
+
element: [
|
|
30
|
+
{
|
|
31
|
+
// Phase 1: unwrap the images inside an images-only paragraph,
|
|
32
|
+
// wrapping each image with alt text in a figure on the way up.
|
|
33
|
+
filter: ["p"],
|
|
34
|
+
visit(node, ctx) {
|
|
35
|
+
if (!hasOnlyImages(node)) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const parent = ctx.parent(node);
|
|
40
|
+
|
|
41
|
+
ctx.replaceNode(
|
|
42
|
+
node,
|
|
43
|
+
node.children
|
|
44
|
+
.filter(
|
|
45
|
+
(child) => child.type === "element" && child.tagName === "img"
|
|
46
|
+
)
|
|
47
|
+
.map((image) =>
|
|
48
|
+
isImageWithAlt(image) &&
|
|
49
|
+
!isImageWithCaption(parent) &&
|
|
50
|
+
!isImageLink(parent)
|
|
51
|
+
? createFigure(image, options)
|
|
52
|
+
: image
|
|
53
|
+
)
|
|
54
|
+
);
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
// Phase 2: wrap every other image with alt text in a figure.
|
|
59
|
+
filter: ["img"],
|
|
60
|
+
visit(node, ctx) {
|
|
61
|
+
if (!isImageWithAlt(node)) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const parent = ctx.parent(node);
|
|
66
|
+
|
|
67
|
+
if (
|
|
68
|
+
isImageWithCaption(parent) ||
|
|
69
|
+
isImageLink(parent) ||
|
|
70
|
+
// Handled by the paragraph visitor above.
|
|
71
|
+
(parent?.tagName === "p" && hasOnlyImages(parent))
|
|
72
|
+
) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
ctx.replaceNode(node, createFigure(node, options));
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
],
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function hasOnlyImages(node) {
|
|
84
|
+
return (
|
|
85
|
+
node?.type === "element" &&
|
|
86
|
+
node.children.every(
|
|
87
|
+
(child) =>
|
|
88
|
+
(child.type === "element" && child.tagName === "img") ||
|
|
89
|
+
(child.type === "text" && whitespaceRe.test(child.value))
|
|
90
|
+
)
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function isImageWithAlt(node) {
|
|
95
|
+
return (
|
|
96
|
+
node?.type === "element" &&
|
|
97
|
+
node.tagName === "img" &&
|
|
98
|
+
Boolean(node.properties?.alt) &&
|
|
99
|
+
Boolean(node.properties?.src)
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function isImageWithCaption(node) {
|
|
104
|
+
return (
|
|
105
|
+
node?.type === "element" &&
|
|
106
|
+
node.tagName === "figure" &&
|
|
107
|
+
node.children.some(
|
|
108
|
+
(child) => child.type === "element" && child.tagName === "figcaption"
|
|
109
|
+
)
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function isImageLink(node) {
|
|
114
|
+
return node?.type === "element" && node.tagName === "a";
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function createFigure(image, options) {
|
|
118
|
+
const classes = toClasses(options.className);
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
type: "element",
|
|
122
|
+
tagName: "figure",
|
|
123
|
+
properties: classes.length > 0 ? { className: classes } : {},
|
|
124
|
+
children: [
|
|
125
|
+
{
|
|
126
|
+
type: "element",
|
|
127
|
+
tagName: "img",
|
|
128
|
+
properties: { ...image.properties },
|
|
129
|
+
children: [],
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
type: "element",
|
|
133
|
+
tagName: "figcaption",
|
|
134
|
+
properties: {},
|
|
135
|
+
children: [{ type: "text", value: String(image.properties.alt) }],
|
|
136
|
+
},
|
|
137
|
+
],
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function toClasses(className) {
|
|
142
|
+
if (!className) {
|
|
143
|
+
return [];
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return (Array.isArray(className) ? className : [className])
|
|
147
|
+
.flatMap((value) => String(value).split(/\s+/))
|
|
148
|
+
.filter(Boolean);
|
|
149
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "satteri-figure",
|
|
3
|
+
"description": "A Satteri plugin to convert images into figures with captions.",
|
|
4
|
+
"version": "0.0.1",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Abhigyan Trips <contact@abhi.now>",
|
|
7
|
+
"repository": "https://github.com/abhigyantrips/satteri-plugins",
|
|
8
|
+
"keywords": [
|
|
9
|
+
"satteri",
|
|
10
|
+
"figure",
|
|
11
|
+
"figcaption",
|
|
12
|
+
"plugin",
|
|
13
|
+
"image",
|
|
14
|
+
"caption"
|
|
15
|
+
],
|
|
16
|
+
"type": "module",
|
|
17
|
+
"main": "index.js",
|
|
18
|
+
"exports": {
|
|
19
|
+
".": "./index.js"
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"index.js"
|
|
23
|
+
],
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"satteri": "^0.10.5"
|
|
26
|
+
},
|
|
27
|
+
"peerDependencies": {
|
|
28
|
+
"satteri": "^0.10.5"
|
|
29
|
+
},
|
|
30
|
+
"scripts": {
|
|
31
|
+
"test": "node --test ./test/plugin.test.js"
|
|
32
|
+
}
|
|
33
|
+
}
|