@reblu/site-blocks 0.4.0 → 0.4.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 CHANGED
@@ -26,6 +26,55 @@ 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
+ ### Ordered lists keep the number you wrote
59
+
60
+ `4. …` renders as a list starting at 4, not at 1 — so "as established in clause
61
+ 4" still points at a `4`. The marker style (`type`) and an item's explicit number
62
+ (`value`) survive too.
63
+
64
+ ### Which authored attributes survive
65
+
66
+ The renderer rebuilds each element to apply the tenant's design tokens and to
67
+ harden external links, and it forwards only the attributes that carry **meaning**:
68
+
69
+ - **Kept:** `id` (so in-document `#anchor` links resolve), `lang`, `dir`,
70
+ `title`, `hidden`, `role`, every `aria-*` and every `data-*`, plus `start`/`type`
71
+ on `<ol>` and `value`/`type` on `<li>`.
72
+ - **Dropped:** `class` and `style` — presentation belongs to the tenant's theme,
73
+ not to the document body — and `target`/`rel` on links, which the renderer sets
74
+ itself so an external link is always `noopener noreferrer`.
75
+ - **Dropped upstream by the sanitiser, before the renderer sees it:** `reversed`
76
+ on `<ol>`. Number a descending list explicitly with `value` on each `<li>`.
77
+
29
78
  ## Security boundaries
30
79
 
31
80
  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)("blockquote", { className: "border-l-4 border-primary/50 pl-6 italic my-6 text-muted-foreground", children: children2() });
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)("pre", { className: "rounded-lg border border-input bg-muted p-4 my-6 overflow-x-auto font-mono text-sm", children: children2() });
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) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", { className: "block", children: children2() });
13095
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", { className: "rounded bg-muted px-1.5 py-0.5 font-mono text-sm", children: children2() });
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
  }