@reblu/site-blocks 0.4.0 → 0.4.2
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 +65 -0
- package/dist/index.cjs +68 -13
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +7 -3
- package/dist/index.d.ts +7 -3
- package/dist/index.js +68 -13
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -26,6 +26,71 @@ a different-looking site for every tenant, with zero forks.
|
|
|
26
26
|
- **`@reblu/site-blocks`** = organisms + headless data hooks. Higher churn — its
|
|
27
27
|
own package so a breaking change in an organism does not bump the stable atoms.
|
|
28
28
|
|
|
29
|
+
## Authoring notes
|
|
30
|
+
|
|
31
|
+
What `MarkdownRenderer` does with tenant-authored Markdown and HTML. These are
|
|
32
|
+
**content** rules, not styling ones — they change what a page says, so they
|
|
33
|
+
matter most on legal pages (privacy policy, terms), which nobody on the platform
|
|
34
|
+
side proofreads before a visitor sees them.
|
|
35
|
+
|
|
36
|
+
### A single newline is not a line break
|
|
37
|
+
|
|
38
|
+
```md
|
|
39
|
+
Calle Mayor 1
|
|
40
|
+
28001 Madrid
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
renders as one paragraph reading `Calle Mayor 1 28001 Madrid`. This is standard
|
|
44
|
+
CommonMark — a lone newline is a *soft* break, i.e. whitespace — and it is what
|
|
45
|
+
this renderer has done since it replaced the interim, non-standard one that
|
|
46
|
+
emitted a `<br>` for every newline. **Text written under the old behaviour reads
|
|
47
|
+
differently now**, so addresses, signature blocks and enumerated clauses typed on
|
|
48
|
+
consecutive lines are worth re-reading.
|
|
49
|
+
|
|
50
|
+
To force a visible break, use any of:
|
|
51
|
+
|
|
52
|
+
| Want | Write |
|
|
53
|
+
| ---- | ----- |
|
|
54
|
+
| A new paragraph | A blank line between the two lines |
|
|
55
|
+
| A hard break inside one paragraph | Two spaces at the end of the first line |
|
|
56
|
+
| A hard break, visibly | A trailing `\` at the end of the first line |
|
|
57
|
+
|
|
58
|
+
### `hidden` hides a block — it does not withhold it
|
|
59
|
+
|
|
60
|
+
An element marked `hidden` is not rendered and is not announced to a screen
|
|
61
|
+
reader. It **is** still present in the HTML the server sends. Anyone who opens
|
|
62
|
+
the page source reads it, and so does any crawler working on raw HTML.
|
|
63
|
+
|
|
64
|
+
That is exactly what the HTML specification says `hidden` means, and for most
|
|
65
|
+
uses it is the right behaviour. It is called out here because the wording in
|
|
66
|
+
`0.4.0`'s changelog — "a block the author marked as not-for-publication stays
|
|
67
|
+
unpublished" — promises more than the attribute delivers, and an author who
|
|
68
|
+
believes it will reach for `hidden` to do something it cannot do.
|
|
69
|
+
|
|
70
|
+
So: a half-written clause left in a privacy policy behind `hidden` is invisible
|
|
71
|
+
on the page and published all the same. **To keep text unpublished, keep it out
|
|
72
|
+
of the page.** Draft it somewhere that is not the document body.
|
|
73
|
+
|
|
74
|
+
### Ordered lists keep the number you wrote
|
|
75
|
+
|
|
76
|
+
`4. …` renders as a list starting at 4, not at 1 — so "as established in clause
|
|
77
|
+
4" still points at a `4`. The marker style (`type`) and an item's explicit number
|
|
78
|
+
(`value`) survive too.
|
|
79
|
+
|
|
80
|
+
### Which authored attributes survive
|
|
81
|
+
|
|
82
|
+
The renderer rebuilds each element to apply the tenant's design tokens and to
|
|
83
|
+
harden external links, and it forwards only the attributes that carry **meaning**:
|
|
84
|
+
|
|
85
|
+
- **Kept:** `id` (so in-document `#anchor` links resolve), `lang`, `dir`,
|
|
86
|
+
`title`, `hidden`, `role`, every `aria-*` and every `data-*`, plus `start`/`type`
|
|
87
|
+
on `<ol>` and `value`/`type` on `<li>`.
|
|
88
|
+
- **Dropped:** `class` and `style` — presentation belongs to the tenant's theme,
|
|
89
|
+
not to the document body — and `target`/`rel` on links, which the renderer sets
|
|
90
|
+
itself so an external link is always `noopener noreferrer`.
|
|
91
|
+
- **Dropped upstream by the sanitiser, before the renderer sees it:** `reversed`
|
|
92
|
+
on `<ol>`. Number a descending list explicitly with `value` on each `<li>`.
|
|
93
|
+
|
|
29
94
|
## Security boundaries
|
|
30
95
|
|
|
31
96
|
Two sinks, two different helpers, in two different packages — do not reach for
|
package/dist/index.cjs
CHANGED
|
@@ -13061,6 +13061,41 @@ var HEADING_CLASSES = {
|
|
|
13061
13061
|
function hasClass(el, name) {
|
|
13062
13062
|
return typeof el.attribs.class === "string" && el.attribs.class.split(/\s+/).includes(name);
|
|
13063
13063
|
}
|
|
13064
|
+
var PRESERVED_GLOBAL_ATTRIBUTES = [
|
|
13065
|
+
// Anchor targets. In-document links ("see clause 4" → `#clause-4`) resolve to
|
|
13066
|
+
// nothing without them.
|
|
13067
|
+
"id",
|
|
13068
|
+
// Language and direction of a passage — a bilingual legal page is the normal case
|
|
13069
|
+
// here, and screen readers switch voice on it.
|
|
13070
|
+
"lang",
|
|
13071
|
+
"dir",
|
|
13072
|
+
// Advisory text the author wrote; it is content, not styling.
|
|
13073
|
+
"title",
|
|
13074
|
+
// Explicit "do not publish this". Without it a block authored as `<p hidden>`
|
|
13075
|
+
// becomes VISIBLE — publishing what the author marked as not-for-publication.
|
|
13076
|
+
"hidden",
|
|
13077
|
+
// Accessible semantics. `../lib/markdown` restores these through the sanitiser
|
|
13078
|
+
// precisely so they are not lost (REB-286's axe gate); dropping them one layer
|
|
13079
|
+
// later would undo that.
|
|
13080
|
+
"role"
|
|
13081
|
+
];
|
|
13082
|
+
var PRESERVED_ATTRIBUTE_PREFIXES = ["aria-", "data-"];
|
|
13083
|
+
var PRESERVED_TAG_ATTRIBUTES = {
|
|
13084
|
+
ol: ["start", "type"],
|
|
13085
|
+
li: ["value", "type"]
|
|
13086
|
+
};
|
|
13087
|
+
var BOOLEAN_ATTRIBUTES = /* @__PURE__ */ new Set(["hidden"]);
|
|
13088
|
+
function authorAttributes(el) {
|
|
13089
|
+
const tagSpecific = PRESERVED_TAG_ATTRIBUTES[el.name] ?? [];
|
|
13090
|
+
const preserved = {};
|
|
13091
|
+
for (const [rawName, value] of Object.entries(el.attribs)) {
|
|
13092
|
+
const name = rawName.toLowerCase();
|
|
13093
|
+
const keep = PRESERVED_GLOBAL_ATTRIBUTES.includes(name) || tagSpecific.includes(name) || PRESERVED_ATTRIBUTE_PREFIXES.some((prefix) => name.startsWith(prefix));
|
|
13094
|
+
if (!keep) continue;
|
|
13095
|
+
preserved[name] = BOOLEAN_ATTRIBUTES.has(name) ? true : value;
|
|
13096
|
+
}
|
|
13097
|
+
return preserved;
|
|
13098
|
+
}
|
|
13064
13099
|
function isExternalHref(href) {
|
|
13065
13100
|
return !!href && /^(https?:)?\/\//.test(href);
|
|
13066
13101
|
}
|
|
@@ -13070,29 +13105,46 @@ function buildOptions(headingOffset) {
|
|
|
13070
13105
|
if (!(node instanceof import_html_react_parser.Element)) return void 0;
|
|
13071
13106
|
const children2 = () => (0, import_html_react_parser.domToReact)(node.children, options);
|
|
13072
13107
|
const { name, attribs } = node;
|
|
13108
|
+
const author = authorAttributes(node);
|
|
13073
13109
|
const headingMatch = /^h([1-6])$/.exec(name);
|
|
13074
13110
|
if (headingMatch) {
|
|
13075
13111
|
const rendered = Math.min(6, Number(headingMatch[1]) + headingOffset);
|
|
13076
13112
|
const Tag = `h${rendered}`;
|
|
13077
|
-
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Tag, { className: HEADING_CLASSES[rendered], children: children2() });
|
|
13113
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Tag, { ...author, className: HEADING_CLASSES[rendered], children: children2() });
|
|
13078
13114
|
}
|
|
13079
13115
|
switch (name) {
|
|
13080
13116
|
case "p":
|
|
13081
|
-
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "my-4 leading-relaxed text-foreground/90", children: children2() });
|
|
13117
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { ...author, className: "my-4 leading-relaxed text-foreground/90", children: children2() });
|
|
13082
13118
|
case "ul":
|
|
13083
|
-
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ul", { className: hasClass(node, "contains-task-list") ? "list-none pl-0 my-4 space-y-1" : "list-disc ml-6 my-4", children: children2() });
|
|
13119
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ul", { ...author, className: hasClass(node, "contains-task-list") ? "list-none pl-0 my-4 space-y-1" : "list-disc ml-6 my-4", children: children2() });
|
|
13084
13120
|
case "ol":
|
|
13085
|
-
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ol", { className: "list-decimal ml-6 my-4", children: children2() });
|
|
13121
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ol", { ...author, className: "list-decimal ml-6 my-4", children: children2() });
|
|
13086
13122
|
case "li":
|
|
13087
|
-
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("li", { className: hasClass(node, "task-list-item") ? "flex items-start gap-2 my-1" : "my-1", children: children2() });
|
|
13123
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("li", { ...author, className: hasClass(node, "task-list-item") ? "flex items-start gap-2 my-1" : "my-1", children: children2() });
|
|
13088
13124
|
case "blockquote":
|
|
13089
|
-
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
13125
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
13126
|
+
"blockquote",
|
|
13127
|
+
{
|
|
13128
|
+
...author,
|
|
13129
|
+
className: "border-l-4 border-primary/50 pl-6 italic my-6 text-muted-foreground",
|
|
13130
|
+
children: children2()
|
|
13131
|
+
}
|
|
13132
|
+
);
|
|
13090
13133
|
case "pre":
|
|
13091
|
-
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
13134
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
13135
|
+
"pre",
|
|
13136
|
+
{
|
|
13137
|
+
...author,
|
|
13138
|
+
className: "rounded-lg border border-input bg-muted p-4 my-6 overflow-x-auto font-mono text-sm",
|
|
13139
|
+
children: children2()
|
|
13140
|
+
}
|
|
13141
|
+
);
|
|
13092
13142
|
case "code": {
|
|
13093
13143
|
const isBlock = node.parent instanceof import_html_react_parser.Element && node.parent.name === "pre";
|
|
13094
|
-
if (isBlock)
|
|
13095
|
-
|
|
13144
|
+
if (isBlock) {
|
|
13145
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", { ...author, className: "block", children: children2() });
|
|
13146
|
+
}
|
|
13147
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", { ...author, className: "rounded bg-muted px-1.5 py-0.5 font-mono text-sm", children: children2() });
|
|
13096
13148
|
}
|
|
13097
13149
|
case "a": {
|
|
13098
13150
|
const href = attribs.href;
|
|
@@ -13100,6 +13152,7 @@ function buildOptions(headingOffset) {
|
|
|
13100
13152
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
13101
13153
|
"a",
|
|
13102
13154
|
{
|
|
13155
|
+
...author,
|
|
13103
13156
|
href,
|
|
13104
13157
|
className: "text-primary underline underline-offset-4 hover:text-primary/80",
|
|
13105
13158
|
...external ? { target: "_blank", rel: "noopener noreferrer" } : {},
|
|
@@ -13116,6 +13169,7 @@ function buildOptions(headingOffset) {
|
|
|
13116
13169
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
13117
13170
|
"img",
|
|
13118
13171
|
{
|
|
13172
|
+
...author,
|
|
13119
13173
|
src: attribs.src,
|
|
13120
13174
|
alt: attribs.alt ?? "",
|
|
13121
13175
|
loading: "lazy",
|
|
@@ -13127,6 +13181,7 @@ function buildOptions(headingOffset) {
|
|
|
13127
13181
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "relative my-6 aspect-video w-full overflow-hidden rounded-lg", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
13128
13182
|
"iframe",
|
|
13129
13183
|
{
|
|
13184
|
+
...author,
|
|
13130
13185
|
src: attribs.src,
|
|
13131
13186
|
title: attribs.title ?? "Embedded video",
|
|
13132
13187
|
allow: attribs.allow ?? "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture",
|
|
@@ -13135,13 +13190,13 @@ function buildOptions(headingOffset) {
|
|
|
13135
13190
|
}
|
|
13136
13191
|
) });
|
|
13137
13192
|
case "hr":
|
|
13138
|
-
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("hr", { className: "my-8 border-muted-foreground/30" });
|
|
13193
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("hr", { ...author, className: "my-8 border-muted-foreground/30" });
|
|
13139
13194
|
case "strong":
|
|
13140
|
-
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { className: "font-bold text-foreground", children: children2() });
|
|
13195
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("strong", { ...author, className: "font-bold text-foreground", children: children2() });
|
|
13141
13196
|
case "em":
|
|
13142
|
-
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("em", { className: "italic", children: children2() });
|
|
13197
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("em", { ...author, className: "italic", children: children2() });
|
|
13143
13198
|
case "del":
|
|
13144
|
-
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("del", { className: "line-through", children: children2() });
|
|
13199
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("del", { ...author, className: "line-through", children: children2() });
|
|
13145
13200
|
default:
|
|
13146
13201
|
return void 0;
|
|
13147
13202
|
}
|