@bytesbrains/weblocks 0.3.0 → 0.4.0
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/AGENT.md +2 -1
- package/CATALOG.md +13 -2
- package/CHANGELOG.md +13 -0
- package/README.md +2 -1
- package/catalog.json +58 -1
- package/lib/blocks/legal.d.ts +2 -0
- package/lib/blocks/legal.js +100 -0
- package/lib/index.d.ts +1 -0
- package/lib/index.js +1 -0
- package/lib/markdown.d.ts +2 -0
- package/lib/markdown.js +90 -0
- package/lib/registry.js +2 -1
- package/package.json +1 -1
package/AGENT.md
CHANGED
|
@@ -46,7 +46,7 @@ its exact config schema:
|
|
|
46
46
|
- `catalogPrompt()` — a compact text menu for a system prompt.
|
|
47
47
|
- `CATALOG.md` — the same, human-readable.
|
|
48
48
|
|
|
49
|
-
The
|
|
49
|
+
The 40 block types, by group:
|
|
50
50
|
|
|
51
51
|
- **Chrome/app-shell:** `nav`, `app-shell`, `sidebar`, `announcement-bar`, `footer`
|
|
52
52
|
- **Heroes:** `hero`, `hero-app`
|
|
@@ -58,6 +58,7 @@ The 39 block types, by group:
|
|
|
58
58
|
- **Collections:** `blog-list`, `blog-post`, `feed`
|
|
59
59
|
- **Dynamic (powered):** `contact-form`, `newsletter`, `search`, `auth`
|
|
60
60
|
- **Conversion/rhythm:** `cta`, `social-links`, `contact-details`, `divider`, `spacer`
|
|
61
|
+
- **Legal:** `legal` (terms/privacy links that open safe-Markdown dialogs; content is Markdown, never raw HTML)
|
|
61
62
|
|
|
62
63
|
## Composing a manifest
|
|
63
64
|
|
package/CATALOG.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
# @bytesbrains/weblocks — Block Catalog (v0.
|
|
1
|
+
# @bytesbrains/weblocks — Block Catalog (v0.4.0)
|
|
2
2
|
|
|
3
3
|
The AI composes a `SiteManifest` (`{ meta, design, blocks[] }`) using **only** the block types below, then the engine validates + renders it to static HTML. This file is generated from the code (`npm run emit:catalog`) — do not edit by hand.
|
|
4
4
|
|
|
5
|
-
**Block types:** `app-shell` · `nav` · `announcement-bar` · `sidebar` · `hero` · `hero-app` · `features` · `about` · `rich-text` · `split` · `steps` · `stats` · `services-catalogue` · `pricing` · `logos` · `team` · `gallery` · `carousel` · `video` · `map` · `timeline` · `tabs` · `accordion` · `testimonials` · `faq` · `blog-list` · `blog-post` · `feed` · `contact-form` · `newsletter` · `search` · `auth` · `cta` · `social-links` · `contact-details` · `directions` · `divider` · `spacer` · `footer`
|
|
5
|
+
**Block types:** `app-shell` · `nav` · `announcement-bar` · `sidebar` · `hero` · `hero-app` · `features` · `about` · `rich-text` · `split` · `steps` · `stats` · `services-catalogue` · `pricing` · `logos` · `team` · `gallery` · `carousel` · `video` · `map` · `timeline` · `tabs` · `accordion` · `testimonials` · `faq` · `blog-list` · `blog-post` · `feed` · `contact-form` · `newsletter` · `search` · `auth` · `cta` · `social-links` · `contact-details` · `directions` · `legal` · `divider` · `spacer` · `footer`
|
|
6
6
|
|
|
7
7
|
## `app-shell`
|
|
8
8
|
|
|
@@ -386,6 +386,17 @@ A location card with deep links that open the visitor’s map app (Google/Apple
|
|
|
386
386
|
| `directionsLabel` | string | | |
|
|
387
387
|
| `appleMaps` | boolean | | |
|
|
388
388
|
|
|
389
|
+
## `legal`
|
|
390
|
+
|
|
391
|
+
Policy links (terms, privacy, cookies, …) that open as scrollable, dismissible dialogs; each body is safe Markdown (never raw HTML). No JavaScript.
|
|
392
|
+
|
|
393
|
+
| field | type | required | notes |
|
|
394
|
+
|---|---|---|---|
|
|
395
|
+
| `title` | string | | |
|
|
396
|
+
| `align` | undefined (start\|center\|end) | | |
|
|
397
|
+
| `separator` | string | | |
|
|
398
|
+
| `documents` | array | | |
|
|
399
|
+
|
|
389
400
|
## `divider`
|
|
390
401
|
|
|
391
402
|
A visual section break rendered as a thin line, a dotted rule, or a gradient bar.
|
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,19 @@ follows [semantic versioning](https://semver.org): the **block catalog** and the
|
|
|
5
5
|
**`SiteManifest` shape** are the public contract — additive block/field changes
|
|
6
6
|
are minor, breaking changes to either are major.
|
|
7
7
|
|
|
8
|
+
## 0.4.0 — 2026-07-16
|
|
9
|
+
|
|
10
|
+
Additive and non-breaking — every `0.3.0` manifest still validates and renders
|
|
11
|
+
identically. Catalog 39 → 40.
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
- **`legal` block** — policy links (Terms, Privacy, Cookies, …) that open as
|
|
15
|
+
**scrollable, dismissible dialogs** (pure CSS `:target`, no JavaScript). Each
|
|
16
|
+
document's body is authored in a **safe Markdown subset** (headings, lists,
|
|
17
|
+
links, emphasis, quotes, rules) — rich formatting, but **never raw HTML** (any
|
|
18
|
+
literal HTML is escaped, not executed). Exports `renderMarkdown`. Catalog now
|
|
19
|
+
40 blocks.
|
|
20
|
+
|
|
8
21
|
## 0.3.0 — 2026-07-16
|
|
9
22
|
|
|
10
23
|
Additive and non-breaking — every `0.2.0` manifest still validates and renders
|
package/README.md
CHANGED
|
@@ -139,7 +139,7 @@ import catalogJson from '@bytesbrains/weblocks/catalog.json' with { type: 'json'
|
|
|
139
139
|
|
|
140
140
|
## Block catalog
|
|
141
141
|
|
|
142
|
-
**
|
|
142
|
+
**40 typed blocks.** Full field reference in [`CATALOG.md`](./CATALOG.md).
|
|
143
143
|
|
|
144
144
|
| Group | Blocks |
|
|
145
145
|
|---|---|
|
|
@@ -152,6 +152,7 @@ import catalogJson from '@bytesbrains/weblocks/catalog.json' with { type: 'json'
|
|
|
152
152
|
| Collections | `blog-list` · `blog-post` · `feed` |
|
|
153
153
|
| Dynamic (powered) | `contact-form` · `newsletter` · `search` · `auth` |
|
|
154
154
|
| Conversion / rhythm | `cta` · `social-links` · `contact-details` · `divider` · `spacer` |
|
|
155
|
+
| Legal | `legal` (terms/privacy links → safe-Markdown dialogs) |
|
|
155
156
|
|
|
156
157
|
`rich-text` and `blog-post` carry **typed** content nodes (headings, paragraphs,
|
|
157
158
|
quotes, lists) — a safe freeform-content escape hatch that is never raw HTML.
|
package/catalog.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
3
|
"package": "@bytesbrains/weblocks",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.4.0",
|
|
5
5
|
"description": "Block catalog for @bytesbrains/weblocks. Send this to the model as its API reference; it composes a SiteManifest ({ meta, design, blocks[] }) using ONLY these block types. The engine then validates and renders it.",
|
|
6
6
|
"blockTypes": [
|
|
7
7
|
"app-shell",
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
"social-links",
|
|
41
41
|
"contact-details",
|
|
42
42
|
"directions",
|
|
43
|
+
"legal",
|
|
43
44
|
"divider",
|
|
44
45
|
"spacer",
|
|
45
46
|
"footer"
|
|
@@ -1862,6 +1863,62 @@
|
|
|
1862
1863
|
"additionalProperties": false
|
|
1863
1864
|
}
|
|
1864
1865
|
},
|
|
1866
|
+
{
|
|
1867
|
+
"type": "legal",
|
|
1868
|
+
"description": "Policy links (terms, privacy, cookies, …) that open as scrollable, dismissible dialogs; each body is safe Markdown (never raw HTML). No JavaScript.",
|
|
1869
|
+
"schema": {
|
|
1870
|
+
"type": "object",
|
|
1871
|
+
"properties": {
|
|
1872
|
+
"title": {
|
|
1873
|
+
"type": "string",
|
|
1874
|
+
"maxLength": 120,
|
|
1875
|
+
"default": ""
|
|
1876
|
+
},
|
|
1877
|
+
"align": {
|
|
1878
|
+
"enum": [
|
|
1879
|
+
"start",
|
|
1880
|
+
"center",
|
|
1881
|
+
"end"
|
|
1882
|
+
],
|
|
1883
|
+
"default": "center"
|
|
1884
|
+
},
|
|
1885
|
+
"separator": {
|
|
1886
|
+
"type": "string",
|
|
1887
|
+
"maxLength": 4,
|
|
1888
|
+
"default": "·"
|
|
1889
|
+
},
|
|
1890
|
+
"documents": {
|
|
1891
|
+
"type": "array",
|
|
1892
|
+
"items": {
|
|
1893
|
+
"type": "object",
|
|
1894
|
+
"properties": {
|
|
1895
|
+
"label": {
|
|
1896
|
+
"type": "string",
|
|
1897
|
+
"maxLength": 60,
|
|
1898
|
+
"default": ""
|
|
1899
|
+
},
|
|
1900
|
+
"title": {
|
|
1901
|
+
"type": "string",
|
|
1902
|
+
"maxLength": 120,
|
|
1903
|
+
"default": ""
|
|
1904
|
+
},
|
|
1905
|
+
"content": {
|
|
1906
|
+
"type": "string",
|
|
1907
|
+
"maxLength": 40000,
|
|
1908
|
+
"default": ""
|
|
1909
|
+
}
|
|
1910
|
+
},
|
|
1911
|
+
"required": [
|
|
1912
|
+
"label"
|
|
1913
|
+
],
|
|
1914
|
+
"additionalProperties": false
|
|
1915
|
+
},
|
|
1916
|
+
"maxItems": 8
|
|
1917
|
+
}
|
|
1918
|
+
},
|
|
1919
|
+
"additionalProperties": false
|
|
1920
|
+
}
|
|
1921
|
+
},
|
|
1865
1922
|
{
|
|
1866
1923
|
"type": "divider",
|
|
1867
1924
|
"description": "A visual section break rendered as a thin line, a dotted rule, or a gradient bar.",
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `legal` — policy links (Terms, Privacy, Cookies, …) that open as **scrollable,
|
|
3
|
+
* dismissible dialogs**. Each document's body is authored in a SAFE Markdown
|
|
4
|
+
* subset (see markdown.ts) — rich formatting, but never raw HTML (any literal
|
|
5
|
+
* HTML is escaped, not executed). Static brick: the dialog is pure CSS
|
|
6
|
+
* (`:target`), so it works with zero JavaScript — click a link to open, click
|
|
7
|
+
* the ✕ or the backdrop to dismiss.
|
|
8
|
+
*
|
|
9
|
+
* Drop it near the `footer` for the familiar "Terms · Privacy" footer row.
|
|
10
|
+
*/
|
|
11
|
+
import { escapeAttr, escapeHtml } from '../schema.js';
|
|
12
|
+
import { renderMarkdown } from '../markdown.js';
|
|
13
|
+
const schema = {
|
|
14
|
+
title: { kind: 'string', default: '', max: 120 },
|
|
15
|
+
align: { kind: 'enum', values: ['start', 'center', 'end'], default: 'center' },
|
|
16
|
+
separator: { kind: 'string', default: '·', max: 4 },
|
|
17
|
+
documents: {
|
|
18
|
+
kind: 'array', max: 8,
|
|
19
|
+
of: {
|
|
20
|
+
kind: 'object',
|
|
21
|
+
fields: {
|
|
22
|
+
label: { kind: 'string', required: true, default: '', max: 60 }, // link text
|
|
23
|
+
title: { kind: 'string', default: '', max: 120 }, // dialog heading (defaults to label)
|
|
24
|
+
content: { kind: 'string', default: '', max: 40000 }, // Markdown body
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
const css = `
|
|
30
|
+
.blk-legal{padding:var(--space) var(--space);background:var(--bg);color:var(--text)}
|
|
31
|
+
.blk-legal .links{display:flex;flex-wrap:wrap;align-items:center;gap:.35em .8em;max-width:1080px;margin:0 auto}
|
|
32
|
+
.blk-legal.align-center .links{justify-content:center}
|
|
33
|
+
.blk-legal.align-end .links{justify-content:flex-end}
|
|
34
|
+
.blk-legal .lead{color:var(--muted);font-size:var(--fs-base)}
|
|
35
|
+
.blk-legal .links a.open{color:var(--muted);text-decoration:none;font-size:var(--fs-base);border-bottom:1px solid transparent}
|
|
36
|
+
.blk-legal .links a.open:hover,.blk-legal .links a.open:focus-visible{color:var(--primary);border-bottom-color:currentColor}
|
|
37
|
+
.blk-legal .sep{color:var(--muted);opacity:.6;user-select:none}
|
|
38
|
+
/* Dialog — hidden until its id is the URL :target. */
|
|
39
|
+
.blk-legal .modal{position:fixed;inset:0;z-index:1000;display:none;padding:calc(var(--space)*1.2)}
|
|
40
|
+
.blk-legal .modal:target{display:grid;place-items:center}
|
|
41
|
+
.blk-legal .backdrop{position:absolute;inset:0;background:color-mix(in srgb,#000 55%,transparent);border:0;display:block}
|
|
42
|
+
.blk-legal .panel{position:relative;display:flex;flex-direction:column;width:min(680px,100%);max-height:85vh;background:var(--surface);color:var(--text);border-radius:var(--radius);box-shadow:0 24px 60px -12px rgba(0,0,0,.5);overflow:hidden}
|
|
43
|
+
.blk-legal .pbar{display:flex;align-items:center;justify-content:space-between;gap:1em;padding:calc(var(--space)*1.1) calc(var(--space)*1.3);border-bottom:1px solid color-mix(in srgb,var(--text) 12%,transparent)}
|
|
44
|
+
.blk-legal .pbar h2{margin:0;font-size:var(--fs-lg);font-weight:800}
|
|
45
|
+
.blk-legal .close{flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;width:2em;height:2em;border-radius:999px;text-decoration:none;color:var(--muted);font-size:1.3em;line-height:1;background:color-mix(in srgb,var(--text) 6%,transparent)}
|
|
46
|
+
.blk-legal .close:hover,.blk-legal .close:focus-visible{color:var(--text);background:color-mix(in srgb,var(--text) 12%,transparent)}
|
|
47
|
+
.blk-legal .body{overflow:auto;padding:calc(var(--space)*1.3);line-height:1.65;overscroll-behavior:contain}
|
|
48
|
+
.blk-legal .body>:first-child{margin-top:0}
|
|
49
|
+
.blk-legal .body>:last-child{margin-bottom:0}
|
|
50
|
+
.blk-legal .body h2{font-size:var(--fs-lg);font-weight:800;margin:1.4em 0 .4em}
|
|
51
|
+
.blk-legal .body h3{font-size:var(--fs-lg);font-weight:700;margin:1.2em 0 .3em}
|
|
52
|
+
.blk-legal .body h4{font-size:var(--fs-base);font-weight:700;margin:1em 0 .3em;text-transform:uppercase;letter-spacing:.03em;color:var(--muted)}
|
|
53
|
+
.blk-legal .body p{margin:0 0 1em}
|
|
54
|
+
.blk-legal .body ul,.blk-legal .body ol{margin:0 0 1em;padding-left:1.4em}
|
|
55
|
+
.blk-legal .body li{margin:.3em 0}
|
|
56
|
+
.blk-legal .body a{color:var(--primary);text-decoration:underline;text-underline-offset:2px}
|
|
57
|
+
.blk-legal .body blockquote{margin:1em 0;padding:.2em 0 .2em 1em;border-left:3px solid var(--accent);color:var(--muted)}
|
|
58
|
+
.blk-legal .body code{background:color-mix(in srgb,var(--text) 8%,transparent);padding:.1em .35em;border-radius:4px;font-size:.9em}
|
|
59
|
+
.blk-legal .body hr{border:0;border-top:1px solid color-mix(in srgb,var(--text) 15%,transparent);margin:1.4em 0}
|
|
60
|
+
/* Lock background scroll while a dialog is open (progressive; no-op if unsupported). */
|
|
61
|
+
html:has(.blk-legal .modal:target){overflow:hidden}
|
|
62
|
+
`.trim();
|
|
63
|
+
function render(config, _tokens, ctx) {
|
|
64
|
+
const bid = String(ctx?.id ?? 'legal').replace(/[^A-Za-z0-9_-]/g, '') || 'legal';
|
|
65
|
+
const title = config.title;
|
|
66
|
+
const align = ['start', 'center', 'end'].includes(config.align) ? config.align : 'center';
|
|
67
|
+
const sep = escapeHtml(config.separator || '·');
|
|
68
|
+
const docs = (config.documents ?? []).filter((d) => d && d.label);
|
|
69
|
+
const links = docs
|
|
70
|
+
.map((d, i) => `<a class="open" id="open-${bid}-${i}" href="#${bid}-${i}">${escapeHtml(d.label)}</a>`)
|
|
71
|
+
.join(`<span class="sep" aria-hidden="true">${sep}</span>`);
|
|
72
|
+
const dialogs = docs.map((d, i) => {
|
|
73
|
+
const did = `${bid}-${i}`;
|
|
74
|
+
const heading = d.title || d.label;
|
|
75
|
+
return `<div class="modal" id="${did}" role="dialog" aria-modal="true" aria-labelledby="h-${did}">
|
|
76
|
+
<a class="backdrop" href="#open-${did}" aria-label="Close" tabindex="-1"></a>
|
|
77
|
+
<div class="panel">
|
|
78
|
+
<div class="pbar">
|
|
79
|
+
<h2 id="h-${did}">${escapeHtml(heading)}</h2>
|
|
80
|
+
<a class="close" href="#open-${did}" aria-label="Close dialog">×</a>
|
|
81
|
+
</div>
|
|
82
|
+
<div class="body">${renderMarkdown(d.content)}</div>
|
|
83
|
+
</div>
|
|
84
|
+
</div>`;
|
|
85
|
+
}).join('\n ');
|
|
86
|
+
return `<section class="blk-legal align-${align}" aria-label="${escapeAttr(title || 'Legal')}">
|
|
87
|
+
<div class="links">
|
|
88
|
+
${title ? `<span class="lead">${escapeHtml(title)}</span>` : ''}
|
|
89
|
+
${links}
|
|
90
|
+
</div>
|
|
91
|
+
${dialogs}
|
|
92
|
+
</section>`;
|
|
93
|
+
}
|
|
94
|
+
export const legal = {
|
|
95
|
+
type: 'legal',
|
|
96
|
+
description: 'Policy links (terms, privacy, cookies, …) that open as scrollable, dismissible dialogs; each body is safe Markdown (never raw HTML). No JavaScript.',
|
|
97
|
+
schema,
|
|
98
|
+
css,
|
|
99
|
+
render,
|
|
100
|
+
};
|
package/lib/index.d.ts
CHANGED
|
@@ -14,6 +14,7 @@ export { NOOP_RUNTIME, pathRuntime, runtimeNeeds, type RuntimeAdapter, type Runt
|
|
|
14
14
|
export { buildWebManifest, buildWebManifestJson, buildServiceWorker, emitPwa, type WebAppManifest, } from './pwa.js';
|
|
15
15
|
export { validateBlock, validateManifest, type Validation } from './validate.js';
|
|
16
16
|
export { parse, escapeHtml, escapeAttr, sanitizeUrl, type Field, type Schema, type ParseResult } from './schema.js';
|
|
17
|
+
export { renderMarkdown } from './markdown.js';
|
|
17
18
|
export { catalog, catalogPrompt, type BlockCatalogEntry, type JsonSchema } from './catalog.js';
|
|
18
19
|
export { applyOp, applyOps, type EditOp, type OpResult, type BatchResult } from './ops.js';
|
|
19
20
|
export { generateSite, editSite, buildGenerationPrompt, buildEditPrompt, parseManifestResponse, parseOpsResponse, type ModelCall, type ComposeResult, type EditResult, } from './generate.js';
|
package/lib/index.js
CHANGED
|
@@ -6,6 +6,7 @@ export { NOOP_RUNTIME, pathRuntime, runtimeNeeds, } from './runtime.js';
|
|
|
6
6
|
export { buildWebManifest, buildWebManifestJson, buildServiceWorker, emitPwa, } from './pwa.js';
|
|
7
7
|
export { validateBlock, validateManifest } from './validate.js';
|
|
8
8
|
export { parse, escapeHtml, escapeAttr, sanitizeUrl } from './schema.js';
|
|
9
|
+
export { renderMarkdown } from './markdown.js';
|
|
9
10
|
export { catalog, catalogPrompt } from './catalog.js';
|
|
10
11
|
export { applyOp, applyOps } from './ops.js';
|
|
11
12
|
export { generateSite, editSite, buildGenerationPrompt, buildEditPrompt, parseManifestResponse, parseOpsResponse, } from './generate.js';
|
package/lib/markdown.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A tiny, SAFE Markdown-subset renderer — escape-first, whitelist-only.
|
|
3
|
+
*
|
|
4
|
+
* This is NOT a raw-HTML passthrough (that would break the engine's no-injection
|
|
5
|
+
* invariant, see VISION.md §3). Every character of the source is HTML-escaped
|
|
6
|
+
* BEFORE any formatting is applied, so a literal `<script>` renders as text,
|
|
7
|
+
* never markup. Only a fixed set of safe elements is ever produced, and link
|
|
8
|
+
* hrefs are scheme-sanitized. Total: it never throws.
|
|
9
|
+
*
|
|
10
|
+
* Supported:
|
|
11
|
+
* #, ##, ###… headings (→ h2…h6)
|
|
12
|
+
* - or * unordered list 1. ordered list
|
|
13
|
+
* > blockquote --- / *** / ___ horizontal rule
|
|
14
|
+
* blank line paragraph break
|
|
15
|
+
* **bold** *italic* _italic_ `code` [text](url)
|
|
16
|
+
*/
|
|
17
|
+
import { escapeHtml, sanitizeUrl } from './schema.js';
|
|
18
|
+
/** Inline formatting. Operates on ALREADY-ESCAPED text; only adds safe tags. */
|
|
19
|
+
function inline(escaped) {
|
|
20
|
+
let s = escaped;
|
|
21
|
+
// `code` first, so its contents aren't touched by the emphasis rules.
|
|
22
|
+
s = s.replace(/`([^`]+)`/g, (_m, c) => `<code>${c}</code>`);
|
|
23
|
+
// [text](url) — url is taken from escaped text (already attribute-safe) and
|
|
24
|
+
// scheme-sanitized; not re-escaped (that would double-encode `&`).
|
|
25
|
+
s = s.replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, (_m, text, url) => `<a href="${sanitizeUrl(url)}" rel="noopener">${text}</a>`);
|
|
26
|
+
s = s.replace(/\*\*([^*]+)\*\*/g, (_m, t) => `<strong>${t}</strong>`);
|
|
27
|
+
s = s.replace(/(^|[^*])\*([^*\s][^*]*?)\*/g, (_m, pre, t) => `${pre}<em>${t}</em>`);
|
|
28
|
+
s = s.replace(/(^|[^_\w])_([^_]+)_/g, (_m, pre, t) => `${pre}<em>${t}</em>`);
|
|
29
|
+
return s;
|
|
30
|
+
}
|
|
31
|
+
const esc = (raw) => inline(escapeHtml(raw));
|
|
32
|
+
/** Render a Markdown-subset string to safe HTML. */
|
|
33
|
+
export function renderMarkdown(src) {
|
|
34
|
+
const lines = String(src ?? '').replace(/\r\n?/g, '\n').split('\n');
|
|
35
|
+
const out = [];
|
|
36
|
+
let para = [];
|
|
37
|
+
const flush = () => { if (para.length) {
|
|
38
|
+
out.push(`<p>${esc(para.join(' '))}</p>`);
|
|
39
|
+
para = [];
|
|
40
|
+
} };
|
|
41
|
+
for (let i = 0; i < lines.length;) {
|
|
42
|
+
const t = lines[i].trim();
|
|
43
|
+
if (!t) {
|
|
44
|
+
flush();
|
|
45
|
+
i++;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (/^(-{3,}|\*{3,}|_{3,})$/.test(t)) {
|
|
49
|
+
flush();
|
|
50
|
+
out.push('<hr>');
|
|
51
|
+
i++;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
const h = /^(#{1,6})\s+(.*)$/.exec(t);
|
|
55
|
+
if (h) {
|
|
56
|
+
flush();
|
|
57
|
+
out.push(`<h${Math.min(h[1].length + 1, 6)}>${esc(h[2].trim())}</h${Math.min(h[1].length + 1, 6)}>`);
|
|
58
|
+
i++;
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (/^>\s?/.test(t)) {
|
|
62
|
+
flush();
|
|
63
|
+
const q = [];
|
|
64
|
+
for (; i < lines.length && /^>\s?/.test(lines[i].trim()); i++)
|
|
65
|
+
q.push(lines[i].trim().replace(/^>\s?/, ''));
|
|
66
|
+
out.push(`<blockquote>${esc(q.join(' '))}</blockquote>`);
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (/^[-*]\s+/.test(t)) {
|
|
70
|
+
flush();
|
|
71
|
+
const items = [];
|
|
72
|
+
for (; i < lines.length && /^[-*]\s+/.test(lines[i].trim()); i++)
|
|
73
|
+
items.push(lines[i].trim().replace(/^[-*]\s+/, ''));
|
|
74
|
+
out.push(`<ul>${items.map((it) => `<li>${esc(it)}</li>`).join('')}</ul>`);
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (/^\d+\.\s+/.test(t)) {
|
|
78
|
+
flush();
|
|
79
|
+
const items = [];
|
|
80
|
+
for (; i < lines.length && /^\d+\.\s+/.test(lines[i].trim()); i++)
|
|
81
|
+
items.push(lines[i].trim().replace(/^\d+\.\s+/, ''));
|
|
82
|
+
out.push(`<ol>${items.map((it) => `<li>${esc(it)}</li>`).join('')}</ol>`);
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
para.push(t);
|
|
86
|
+
i++;
|
|
87
|
+
}
|
|
88
|
+
flush();
|
|
89
|
+
return out.join('\n');
|
|
90
|
+
}
|
package/lib/registry.js
CHANGED
|
@@ -32,6 +32,7 @@ import { cta } from './blocks/cta.js';
|
|
|
32
32
|
import { socialLinks } from './blocks/socialLinks.js';
|
|
33
33
|
import { contactDetails } from './blocks/contactDetails.js';
|
|
34
34
|
import { directions } from './blocks/directions.js';
|
|
35
|
+
import { legal } from './blocks/legal.js';
|
|
35
36
|
import { divider } from './blocks/divider.js';
|
|
36
37
|
import { spacer } from './blocks/spacer.js';
|
|
37
38
|
import { appShell } from './blocks/appShell.js';
|
|
@@ -56,7 +57,7 @@ const SPECS = [
|
|
|
56
57
|
// dynamic / powered
|
|
57
58
|
contactForm, newsletter, search, auth,
|
|
58
59
|
// conversion / contact
|
|
59
|
-
cta, socialLinks, contactDetails, directions,
|
|
60
|
+
cta, socialLinks, contactDetails, directions, legal,
|
|
60
61
|
// rhythm
|
|
61
62
|
divider, spacer,
|
|
62
63
|
footer,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bytesbrains/weblocks",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Block engine for AI-composable web apps — the AI composes a SiteManifest from a fixed block catalog; the engine validates and renders it to static HTML. Snap-together \"Lego\" web building blocks, shareable across repos.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|