@measured/puck-plugin-heading-analyzer 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
package/README.md ADDED
@@ -0,0 +1,33 @@
1
+ # plugin-heading-analyzer
2
+
3
+ Visualise your heading outline structure and identify missing heading levels. Respects WCAG 2.
4
+
5
+ <img src="https://i.imgur.com/POqtgHu.jpg" alt="example" width="156px" />
6
+
7
+ ## Quick start
8
+
9
+ ```sh
10
+ npm i @puck/plugin-heading-analyzer
11
+ ```
12
+
13
+ ```jsx
14
+ import { Puck } from "@puck/puck";
15
+ import headingAnalyzer from "@puck/plugin-heading-analyzer";
16
+
17
+ ...
18
+
19
+ // Render Puck
20
+ export function Page() {
21
+ return <Puck
22
+ config={config}
23
+ data={data}
24
+ plugins={[
25
+ headingAnalyzer
26
+ ]}
27
+ />;
28
+ }
29
+ ```
30
+
31
+ ## License
32
+
33
+ MIT © [Measured Co.](https://github.com/measuredco)
package/dist/index.css ADDED
@@ -0,0 +1,101 @@
1
+ /* css-module:/Users/chrisvilla/Projects/puck/packages/core/SidebarSection/styles.module.css/#css-module-data */
2
+ ._SidebarSection_jsf8x_1:last-of-type ._SidebarSection-content_jsf8x_1 {
3
+ border-bottom: none;
4
+ height: 100%;
5
+ }
6
+ ._SidebarSection-title_jsf8x_6 {
7
+ background: white;
8
+ padding: 16px;
9
+ border-bottom: 1px solid var(--puck-color-grey-8);
10
+ }
11
+ ._SidebarSection-title_jsf8x_6:hover {
12
+ opacity: 0.6;
13
+ cursor: pointer;
14
+ }
15
+ ._SidebarSection-content_jsf8x_1 {
16
+ border-bottom: 1px solid var(--puck-color-grey-8);
17
+ padding: 16px;
18
+ }
19
+ ._SidebarSection_jsf8x_1 > summary {
20
+ list-style: none;
21
+ }
22
+ ._SidebarSection_jsf8x_1 > summary::-webkit-details-marker {
23
+ display: none;
24
+ }
25
+
26
+ /* css-module:/Users/chrisvilla/Projects/puck/packages/core/Heading/styles.module.css/#css-module-data */
27
+ ._Heading_1y35v_1 {
28
+ display: block;
29
+ font-family: var(--puck-font-stack);
30
+ font-weight: 700;
31
+ margin: 0;
32
+ }
33
+ ._Heading_1y35v_1 b {
34
+ font-weight: 700;
35
+ }
36
+ ._Heading--xxxxl_1y35v_12 {
37
+ font-size: var(--puck-font-size-xxxxl);
38
+ letter-spacing: 0.08ch;
39
+ font-weight: 800;
40
+ }
41
+ ._Heading--xxxl_1y35v_18 {
42
+ font-size: var(--puck-font-size-xxxl);
43
+ }
44
+ ._Heading--xxl_1y35v_22 {
45
+ font-size: var(--puck-font-size-xxl);
46
+ }
47
+ ._Heading--xl_1y35v_26 {
48
+ font-size: var(--puck-font-size-xl);
49
+ }
50
+ ._Heading--l_1y35v_30 {
51
+ font-size: var(--puck-font-size-l);
52
+ }
53
+ ._Heading--m_1y35v_34 {
54
+ font-size: var(--puck-font-size-m);
55
+ }
56
+ ._Heading--s_1y35v_38 {
57
+ font-size: var(--puck-font-size-s);
58
+ }
59
+ ._Heading--xs_1y35v_42 {
60
+ font-size: var(--puck-font-size-xs);
61
+ }
62
+
63
+ /* css-module:/Users/chrisvilla/Projects/puck/packages/core/OutlineList/styles.module.css/#css-module-data */
64
+ ._OutlineList_gor6f_1 {
65
+ color: var(--puck-color-grey-2);
66
+ font-family: var(--puck-font-stack);
67
+ margin: 0;
68
+ margin-left: 16px;
69
+ padding-left: 16px;
70
+ position: relative;
71
+ list-style: none;
72
+ }
73
+ ._OutlineList_gor6f_1::before {
74
+ background: var(--puck-color-grey-7);
75
+ position: absolute;
76
+ left: -1px;
77
+ top: 0px;
78
+ width: 1px;
79
+ height: calc(100% - 9px);
80
+ content: "";
81
+ }
82
+ ._OutlineList_gor6f_1 li {
83
+ position: relative;
84
+ margin-bottom: 4px;
85
+ }
86
+ ._OutlineList_gor6f_1 li::before {
87
+ background: var(--puck-color-grey-7);
88
+ position: absolute;
89
+ left: -17px;
90
+ top: 9px;
91
+ width: 13px;
92
+ height: 1px;
93
+ content: "";
94
+ }
95
+ ._OutlineList_gor6f_1 ._OutlineList-clickableItem_gor6f_36:hover {
96
+ color: var(--puck-color-blue);
97
+ cursor: pointer;
98
+ }
99
+ ._OutlineList_gor6f_1 li > ._OutlineList_gor6f_1 {
100
+ margin: 8px;
101
+ }
@@ -0,0 +1,5 @@
1
+ import { Plugin } from '@puck/core/types/Plugin';
2
+
3
+ declare const HeadingAnalyzer: Plugin;
4
+
5
+ export { HeadingAnalyzer as default };
package/dist/index.js ADDED
@@ -0,0 +1,374 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getOwnPropSymbols = Object.getOwnPropertySymbols;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __propIsEnum = Object.prototype.propertyIsEnumerable;
10
+ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
11
+ var __spreadValues = (a, b) => {
12
+ for (var prop in b || (b = {}))
13
+ if (__hasOwnProp.call(b, prop))
14
+ __defNormalProp(a, prop, b[prop]);
15
+ if (__getOwnPropSymbols)
16
+ for (var prop of __getOwnPropSymbols(b)) {
17
+ if (__propIsEnum.call(b, prop))
18
+ __defNormalProp(a, prop, b[prop]);
19
+ }
20
+ return a;
21
+ };
22
+ var __esm = (fn, res) => function __init() {
23
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
24
+ };
25
+ var __commonJS = (cb, mod) => function __require() {
26
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
27
+ };
28
+ var __export = (target, all) => {
29
+ for (var name in all)
30
+ __defProp(target, name, { get: all[name], enumerable: true });
31
+ };
32
+ var __copyProps = (to, from, except, desc) => {
33
+ if (from && typeof from === "object" || typeof from === "function") {
34
+ for (let key of __getOwnPropNames(from))
35
+ if (!__hasOwnProp.call(to, key) && key !== except)
36
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
37
+ }
38
+ return to;
39
+ };
40
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
41
+ // If the importer is in node compatibility mode or this is not an ESM
42
+ // file that has been converted to a CommonJS file using a Babel-
43
+ // compatible transform (i.e. "__esModule" has not been set), then set
44
+ // "default" to the CommonJS "module.exports" for node compatibility.
45
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
46
+ mod
47
+ ));
48
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
49
+
50
+ // ../tsup-config/react-import.js
51
+ var import_react;
52
+ var init_react_import = __esm({
53
+ "../tsup-config/react-import.js"() {
54
+ "use strict";
55
+ import_react = __toESM(require("react"));
56
+ }
57
+ });
58
+
59
+ // ../../node_modules/classnames/index.js
60
+ var require_classnames = __commonJS({
61
+ "../../node_modules/classnames/index.js"(exports, module2) {
62
+ init_react_import();
63
+ (function() {
64
+ "use strict";
65
+ var hasOwn = {}.hasOwnProperty;
66
+ var nativeCodeString = "[native code]";
67
+ function classNames() {
68
+ var classes = [];
69
+ for (var i = 0; i < arguments.length; i++) {
70
+ var arg = arguments[i];
71
+ if (!arg)
72
+ continue;
73
+ var argType = typeof arg;
74
+ if (argType === "string" || argType === "number") {
75
+ classes.push(arg);
76
+ } else if (Array.isArray(arg)) {
77
+ if (arg.length) {
78
+ var inner = classNames.apply(null, arg);
79
+ if (inner) {
80
+ classes.push(inner);
81
+ }
82
+ }
83
+ } else if (argType === "object") {
84
+ if (arg.toString !== Object.prototype.toString && !arg.toString.toString().includes("[native code]")) {
85
+ classes.push(arg.toString());
86
+ continue;
87
+ }
88
+ for (var key in arg) {
89
+ if (hasOwn.call(arg, key) && arg[key]) {
90
+ classes.push(key);
91
+ }
92
+ }
93
+ }
94
+ }
95
+ return classes.join(" ");
96
+ }
97
+ if (typeof module2 !== "undefined" && module2.exports) {
98
+ classNames.default = classNames;
99
+ module2.exports = classNames;
100
+ } else if (typeof define === "function" && typeof define.amd === "object" && define.amd) {
101
+ define("classnames", [], function() {
102
+ return classNames;
103
+ });
104
+ } else {
105
+ window.classNames = classNames;
106
+ }
107
+ })();
108
+ }
109
+ });
110
+
111
+ // index.tsx
112
+ var plugin_heading_analyzer_exports = {};
113
+ __export(plugin_heading_analyzer_exports, {
114
+ default: () => plugin_heading_analyzer_default
115
+ });
116
+ module.exports = __toCommonJS(plugin_heading_analyzer_exports);
117
+ init_react_import();
118
+ var import_react2 = require("react");
119
+
120
+ // ../core/SidebarSection/index.tsx
121
+ init_react_import();
122
+
123
+ // css-module:/Users/chrisvilla/Projects/puck/packages/core/SidebarSection/styles.module.css#css-module
124
+ init_react_import();
125
+ var styles_module_default = { "SidebarSection": "_SidebarSection_jsf8x_1", "SidebarSection-content": "_SidebarSection-content_jsf8x_1", "SidebarSection-title": "_SidebarSection-title_jsf8x_6" };
126
+
127
+ // ../core/lib/get-class-name-factory.ts
128
+ init_react_import();
129
+ var import_classnames = __toESM(require_classnames());
130
+ var getClassNameFactory = (rootClass, styles, { baseClass = "" } = {}) => (options = {}) => {
131
+ let descendant = false;
132
+ let modifiers = false;
133
+ if (typeof options === "string") {
134
+ descendant = options;
135
+ } else if (typeof options === "object") {
136
+ modifiers = options;
137
+ }
138
+ if (descendant) {
139
+ return baseClass + styles[`${rootClass}-${descendant}`] || "";
140
+ } else if (modifiers) {
141
+ const prefixedModifiers = {};
142
+ for (let modifier in modifiers) {
143
+ prefixedModifiers[styles[`${rootClass}--${modifier}`]] = modifiers[modifier];
144
+ }
145
+ const c = styles[rootClass];
146
+ return baseClass + (0, import_classnames.default)(__spreadValues({
147
+ [c]: !!c
148
+ }, prefixedModifiers));
149
+ } else {
150
+ return baseClass + styles[rootClass] || "";
151
+ }
152
+ };
153
+ var get_class_name_factory_default = getClassNameFactory;
154
+
155
+ // ../core/Heading/index.tsx
156
+ init_react_import();
157
+
158
+ // css-module:/Users/chrisvilla/Projects/puck/packages/core/Heading/styles.module.css#css-module
159
+ init_react_import();
160
+ var styles_module_default2 = { "Heading": "_Heading_1y35v_1", "Heading--xxxxl": "_Heading--xxxxl_1y35v_12", "Heading--xxxl": "_Heading--xxxl_1y35v_18", "Heading--xxl": "_Heading--xxl_1y35v_22", "Heading--xl": "_Heading--xl_1y35v_26", "Heading--l": "_Heading--l_1y35v_30", "Heading--m": "_Heading--m_1y35v_34", "Heading--s": "_Heading--s_1y35v_38", "Heading--xs": "_Heading--xs_1y35v_42" };
161
+
162
+ // ../core/Heading/index.tsx
163
+ var import_jsx_runtime = require("react/jsx-runtime");
164
+ var getClassName = get_class_name_factory_default("Heading", styles_module_default2);
165
+ var Heading = ({ children, rank, size = "m" }) => {
166
+ const Tag = rank ? `h${rank}` : "span";
167
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
168
+ Tag,
169
+ {
170
+ className: getClassName({
171
+ [size]: true
172
+ }),
173
+ children
174
+ }
175
+ );
176
+ };
177
+
178
+ // ../core/SidebarSection/index.tsx
179
+ var getClassName2 = get_class_name_factory_default("SidebarSection", styles_module_default);
180
+ var SidebarSection = ({
181
+ children,
182
+ title
183
+ }) => {
184
+ return /* @__PURE__ */ import_react.default.createElement("details", { className: getClassName2({}), open: true }, /* @__PURE__ */ import_react.default.createElement("summary", { className: getClassName2("title") }, /* @__PURE__ */ import_react.default.createElement(Heading, { rank: 2, size: "xs" }, title)), /* @__PURE__ */ import_react.default.createElement("div", { className: getClassName2("content") }, children));
185
+ };
186
+
187
+ // ../core/OutlineList/index.tsx
188
+ init_react_import();
189
+
190
+ // css-module:/Users/chrisvilla/Projects/puck/packages/core/OutlineList/styles.module.css#css-module
191
+ init_react_import();
192
+ var styles_module_default3 = { "OutlineList": "_OutlineList_gor6f_1", "OutlineList-clickableItem": "_OutlineList-clickableItem_gor6f_36" };
193
+
194
+ // ../core/OutlineList/index.tsx
195
+ var getClassName3 = get_class_name_factory_default("OutlineList", styles_module_default3);
196
+ var OutlineList = ({ children }) => {
197
+ return /* @__PURE__ */ import_react.default.createElement("ul", { className: getClassName3() }, children);
198
+ };
199
+ OutlineList.Clickable = ({ children }) => /* @__PURE__ */ import_react.default.createElement("div", { className: getClassName3("clickableItem") }, children);
200
+ OutlineList.Item = ({
201
+ children,
202
+ onClick
203
+ }) => {
204
+ return /* @__PURE__ */ import_react.default.createElement(
205
+ "li",
206
+ {
207
+ className: onClick ? getClassName3("clickableItem") : "",
208
+ onClick
209
+ },
210
+ children
211
+ );
212
+ };
213
+
214
+ // ../core/lib/scroll-into-view.ts
215
+ init_react_import();
216
+ var scrollIntoView = (el) => {
217
+ const oldStyle = __spreadValues({}, el.style);
218
+ el.style.scrollMargin = "256px";
219
+ if (el) {
220
+ el == null ? void 0 : el.scrollIntoView({ behavior: "smooth" });
221
+ el.style.scrollMargin = oldStyle.scrollMargin || "";
222
+ }
223
+ };
224
+
225
+ // index.tsx
226
+ var import_react_from_json = __toESM(require("react-from-json"));
227
+ var import_jsx_runtime2 = require("react/jsx-runtime");
228
+ var dataAttr = "data-puck-heading-analyzer-id";
229
+ var getOutline = ({
230
+ addDataAttr = false
231
+ } = {}) => {
232
+ const headings = window.document.querySelector(".puck-root").querySelectorAll("h1,h2,h3,h4,h5,h6");
233
+ const _outline = [];
234
+ headings.forEach((item, i) => {
235
+ if (addDataAttr) {
236
+ item.setAttribute(dataAttr, i.toString());
237
+ }
238
+ _outline.push({
239
+ rank: parseInt(item.tagName.split("H")[1]),
240
+ text: item.textContent,
241
+ analyzeId: i.toString()
242
+ });
243
+ });
244
+ return _outline;
245
+ };
246
+ function buildHierarchy() {
247
+ var _a, _b;
248
+ const headings = getOutline({ addDataAttr: true });
249
+ const root = { rank: 0, children: [], text: "" };
250
+ let path = [root];
251
+ for (let heading of headings) {
252
+ const node = {
253
+ rank: heading.rank,
254
+ text: heading.text,
255
+ analyzeId: heading.analyzeId,
256
+ children: []
257
+ };
258
+ if (node.rank === 1) {
259
+ path = [root];
260
+ } else {
261
+ while (path[path.length - 1].rank >= node.rank) {
262
+ path.pop();
263
+ }
264
+ while (path.length < node.rank) {
265
+ const missingNode = {
266
+ rank: path.length,
267
+ missing: true,
268
+ children: [],
269
+ text: ""
270
+ };
271
+ (_a = path[path.length - 1].children) == null ? void 0 : _a.push(missingNode);
272
+ path.push(missingNode);
273
+ }
274
+ }
275
+ (_b = path[path.length - 1].children) == null ? void 0 : _b.push(node);
276
+ path.push(node);
277
+ }
278
+ return root.children;
279
+ }
280
+ var HeadingOutlineAnalyser = ({
281
+ children,
282
+ data
283
+ }) => {
284
+ const [hierarchy, setHierarchy] = (0, import_react2.useState)([]);
285
+ const [firstRender, setFirstRender] = (0, import_react2.useState)(true);
286
+ (0, import_react2.useEffect)(() => {
287
+ if (firstRender) {
288
+ setTimeout(() => {
289
+ setHierarchy(buildHierarchy());
290
+ setFirstRender(false);
291
+ }, 100);
292
+ } else {
293
+ setHierarchy(buildHierarchy());
294
+ }
295
+ }, [data.content]);
296
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { children: [
297
+ children,
298
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(SidebarSection, { title: "Heading Outline", children: [
299
+ hierarchy.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { children: "No headings." }),
300
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(OutlineList, { children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
301
+ import_react_from_json.default,
302
+ {
303
+ mapping: {
304
+ Root: (props) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_jsx_runtime2.Fragment, { children: props.children }),
305
+ OutlineListItem: (props) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(OutlineList.Item, { children: [
306
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(OutlineList.Clickable, { children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
307
+ "small",
308
+ {
309
+ onClick: typeof props.analyzeId == "undefined" ? void 0 : (e) => {
310
+ e.stopPropagation();
311
+ const el = document.querySelector(
312
+ `[${dataAttr}="${props.analyzeId}"]`
313
+ );
314
+ const oldStyle = __spreadValues({}, el.style);
315
+ if (el) {
316
+ scrollIntoView(el);
317
+ el.style.outline = "4px solid var(--puck-color-rose-5)";
318
+ el.style.outlineOffset = "4px";
319
+ setTimeout(() => {
320
+ el.style.outline = oldStyle.outline || "";
321
+ el.style.outlineOffset = oldStyle.outlineOffset || "";
322
+ }, 2e3);
323
+ }
324
+ },
325
+ children: props.missing ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { style: { color: "var(--puck-color-red)" }, children: [
326
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("b", { children: [
327
+ "H",
328
+ props.rank
329
+ ] }),
330
+ ": Missing"
331
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { children: [
332
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("b", { children: [
333
+ "H",
334
+ props.rank
335
+ ] }),
336
+ ": ",
337
+ props.text
338
+ ] })
339
+ }
340
+ ) }),
341
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(OutlineList, { children: props.children })
342
+ ] })
343
+ },
344
+ entry: {
345
+ props: { children: hierarchy },
346
+ type: "Root"
347
+ },
348
+ mapProp: (prop) => {
349
+ if (prop && prop.rank) {
350
+ return {
351
+ type: "OutlineListItem",
352
+ props: prop
353
+ };
354
+ }
355
+ return prop;
356
+ }
357
+ }
358
+ ) })
359
+ ] })
360
+ ] });
361
+ };
362
+ var HeadingAnalyzer = {
363
+ renderPageFields: HeadingOutlineAnalyser
364
+ };
365
+ var plugin_heading_analyzer_default = HeadingAnalyzer;
366
+ /*! Bundled license information:
367
+
368
+ classnames/index.js:
369
+ (*!
370
+ Copyright (c) 2018 Jed Watson.
371
+ Licensed under the MIT License (MIT), see
372
+ http://jedwatson.github.io/classnames
373
+ *)
374
+ */
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "@measured/puck-plugin-heading-analyzer",
3
+ "version": "0.1.0",
4
+ "private": false,
5
+ "main": "./dist/index.js",
6
+ "types": "./dist/index.d.ts",
7
+ "license": "MIT",
8
+ "scripts": {
9
+ "lint": "eslint \"**/*.ts*\"",
10
+ "build": "rm -rf dist && tsup index.tsx"
11
+ },
12
+ "files": [
13
+ "dist"
14
+ ],
15
+ "devDependencies": {
16
+ "@types/react": "^18.2.0",
17
+ "@types/react-dom": "^18.2.0",
18
+ "@puck/core": "*",
19
+ "eslint": "^7.32.0",
20
+ "eslint-config-custom": "*",
21
+ "tsconfig": "*",
22
+ "tsup-config": "*",
23
+ "typescript": "^4.5.2"
24
+ },
25
+ "dependencies": {
26
+ "react-from-json": "^0.7.2"
27
+ },
28
+ "peerDependencies": {
29
+ "react": "^17.0.2"
30
+ }
31
+ }