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.
Files changed (3) hide show
  1. package/README.md +64 -0
  2. package/index.js +149 -0
  3. 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., `![Alt text](path-to-image.jpg)`) 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("![Alt text](path-to-image.jpg)", {
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
+ }