@lowdefy/blocks-basic 0.0.0-experimental-20260908154018 → 0.0.0-experimental-20260909123240

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.
@@ -0,0 +1,36 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */ import { htmlToText, isBlank } from '@lowdefy/block-utils/report';
16
+ /**
17
+ * Box → a `stack` of its `content` area. The Box block shows
18
+ * `properties.content` when set instead of its child area, so the report
19
+ * mirrors that: a content string becomes a single `text` (markup flattened),
20
+ * otherwise the walked area passes through as a stack. An empty, content-less
21
+ * box yields no node.
22
+ */ export const Box = {
23
+ toReport: ({ block, areas = {} })=>{
24
+ const content = block.properties?.content;
25
+ if (!isBlank(content)) return {
26
+ kind: 'text',
27
+ text: htmlToText(content)
28
+ };
29
+ const nodes = areas.content ?? [];
30
+ if (nodes.length === 0) return null;
31
+ return {
32
+ kind: 'stack',
33
+ children: nodes
34
+ };
35
+ }
36
+ };
@@ -14,6 +14,7 @@
14
14
  limitations under the License.
15
15
  */ export default {
16
16
  category: 'container',
17
+ static: true,
17
18
  icons: [],
18
19
  valueType: null,
19
20
  slots: {
@@ -17,20 +17,13 @@ import DOMPurify from 'dompurify';
17
17
  import { withBlockDefaults } from '@lowdefy/block-utils';
18
18
  import { type } from '@lowdefy/helpers';
19
19
  let DangerousHtml = class DangerousHtml extends React.Component {
20
- sanitize() {
21
- const { html, DOMPurifyOptions } = this.props.properties;
22
- if (!this.warnedOptionsChange && JSON.stringify(DOMPurifyOptions) !== JSON.stringify(this.DOMPurifyOptions)) {
23
- this.warnedOptionsChange = true;
24
- console.warn(`DangerousHtml block "${this.props.blockId}": DOMPurifyOptions changed after mount and the change is ignored. The sanitizer options are fixed at the first render; write them as a literal, not from state.`);
25
- }
26
- const htmlString = type.isNone(html) ? '' : html.toString();
27
- this.div.innerHTML = DOMPurify.sanitize(htmlString, this.DOMPurifyOptions);
28
- }
29
20
  componentDidMount() {
30
- this.sanitize();
21
+ const htmlString = type.isNone(this.props.properties.html) ? '' : this.props.properties.html.toString();
22
+ this.div.innerHTML = DOMPurify.sanitize(htmlString, this.DOMPurifyOptions);
31
23
  }
32
24
  componentDidUpdate() {
33
- this.sanitize();
25
+ const htmlString = type.isNone(this.props.properties.html) ? '' : this.props.properties.html.toString();
26
+ this.div.innerHTML = DOMPurify.sanitize(htmlString, this.DOMPurifyOptions);
34
27
  }
35
28
  render() {
36
29
  const { blockId, classNames, styles } = this.props;
@@ -51,11 +44,8 @@ let DangerousHtml = class DangerousHtml extends React.Component {
51
44
  this.div = {
52
45
  innerHTML: ''
53
46
  };
54
- // The sanitizer options are fixed at mount on purpose: a later, operator-driven
55
- // value could loosen the sanitizer from state. A change is refused loudly
56
- // rather than ignored silently.
47
+ // we do not revaluate DOMPurifyOptions improve options safety by not making options dynamic.
57
48
  this.DOMPurifyOptions = props.properties.DOMPurifyOptions;
58
- this.warnedOptionsChange = false;
59
49
  }
60
50
  };
61
51
  export default withBlockDefaults(DangerousHtml);
@@ -14,23 +14,12 @@
14
14
  limitations under the License.
15
15
  */ export default {
16
16
  category: 'display',
17
+ static: true,
17
18
  icons: [],
18
19
  valueType: null,
19
20
  cssKeys: {
20
21
  element: 'The DangerousHtml element.'
21
22
  },
22
- hazards: [
23
- {
24
- id: 'dangerous-html-unsanitised',
25
- message: 'Content is rendered without sanitising, so never pass caller-supplied or database HTML through it. Use Html for anything a user could have written.',
26
- see: 'display-blocks/dangeroushtml'
27
- },
28
- {
29
- id: 'dompurify-options-first-render',
30
- message: 'DOMPurifyOptions is read once when the block mounts, so a value that changes after the first render has no effect. Set it statically or remount the block.',
31
- see: 'display-blocks/dangeroushtml'
32
- }
33
- ],
34
23
  properties: {
35
24
  type: 'object',
36
25
  additionalProperties: false,
@@ -0,0 +1,95 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */ /**
16
+ * Html / DangerousHtml → `svg`.
17
+ *
18
+ * Reports use Html blocks for custom components (KPI tiles, badges, styled
19
+ * headings), so the report renders the markup rather than skipping it. The
20
+ * layout engine belongs to the reports plugin, which hands it in as
21
+ * `context.renderHtml`: this renderer only decides the box (the column width
22
+ * and the block's own style height) and reports what the engine cannot draw.
23
+ * Keeping the engine out of this package means an app that never renders a
24
+ * report installs no native layout binary for its Html blocks.
25
+ *
26
+ * `DangerousHtml` shares this renderer: sanitization is a client concern, so
27
+ * the report renders the same string the page would.
28
+ */ import { type } from '@lowdefy/helpers';
29
+ import { isBlank, styleValue, toPoints } from '@lowdefy/block-utils/report';
30
+ // The html engine has no table layout algorithm, so `<table>` cells run on
31
+ // inline. The block renders, mislaid out, with this warning naming the fix.
32
+ const TABLE_PATTERN = /<table[\s/>]/i;
33
+ // The html engine never fetches an image, so an `<img>` draws nothing and takes
34
+ // no space. Warn rather than let a logo vanish silently.
35
+ const IMG_PATTERN = /<img[\s/>]/i;
36
+ let missingRendererWarned = false;
37
+ function warnMissingRenderer({ block, context }) {
38
+ if (missingRendererWarned) return;
39
+ missingRendererWarned = true;
40
+ context?.logger?.warn?.({
41
+ blockId: block.blockId
42
+ }, `${block.type} block '${block.blockId}' skipped: the reports plugin supplied no html renderer (context.renderHtml). Html blocks render only through @lowdefy/plugin-reports.`);
43
+ }
44
+ const toReport = async ({ block, layout, context })=>{
45
+ const html = block.properties?.html;
46
+ if (isBlank(html)) return null;
47
+ const source = String(html);
48
+ if (!type.isFunction(context?.renderHtml)) {
49
+ warnMissingRenderer({
50
+ block,
51
+ context
52
+ });
53
+ return null;
54
+ }
55
+ if (TABLE_PATTERN.test(source)) {
56
+ context.logger?.warn?.({
57
+ blockId: block.blockId
58
+ }, `${block.type} block '${block.blockId}' contains <table> markup, which reports cannot lay out: cells run on inline. Use flex markup, or a table block, for tabular report content.`);
59
+ }
60
+ if (IMG_PATTERN.test(source)) {
61
+ context.logger?.warn?.({
62
+ blockId: block.blockId
63
+ }, `${block.type} block '${block.blockId}' contains <img> markup, which reports cannot load: the image draws nothing and takes no space. Use an Img block for report images.`);
64
+ }
65
+ const width = layout.width;
66
+ // The block's own style height wins over auto-measurement, so tiles in a row
67
+ // line up even when one label wraps.
68
+ const styleHeight = toPoints(styleValue(block.style, 'height'));
69
+ try {
70
+ const { svg, height } = await context.renderHtml({
71
+ html: source,
72
+ width,
73
+ height: styleHeight
74
+ });
75
+ // Markup that measures to nothing (an empty div, a comment) has nothing to
76
+ // draw, and a zero-height node would still take its margin in the PDF.
77
+ if (height === 0) return null;
78
+ return {
79
+ kind: 'svg',
80
+ svg,
81
+ width,
82
+ height
83
+ };
84
+ } catch (error) {
85
+ context.logger?.warn?.({
86
+ blockId: block.blockId,
87
+ err: error
88
+ }, `${block.type} block '${block.blockId}' failed to render and was skipped: ${error.message}`);
89
+ return null;
90
+ }
91
+ };
92
+ export const Html = {
93
+ toReport
94
+ };
95
+ /** DangerousHtml renders through the same engine, from the same property. */ export const DangerousHtml = Html;
@@ -14,18 +14,12 @@
14
14
  limitations under the License.
15
15
  */ export default {
16
16
  category: 'display',
17
+ static: true,
17
18
  icons: [],
18
19
  valueType: null,
19
20
  cssKeys: {
20
21
  element: 'The Html element.'
21
22
  },
22
- hazards: [
23
- {
24
- id: 'html-style-stripped',
25
- message: 'Content is sanitised with DOMPurify, so <style>, <script> and inline event handlers are removed before render. Use DangerousHtml for a trusted <style> block, or style the block through its style and class properties.',
26
- see: 'display-blocks/html'
27
- }
28
- ],
29
23
  events: {
30
24
  onTextSelection: 'Trigger action when text is selected and pass selected text to the event object.'
31
25
  },
@@ -0,0 +1,108 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */ /**
16
+ * Icon → `svg`. Production KPI tiles pair a number with a movement arrow, so an
17
+ * unrendered Icon costs the report the direction of every trend it shows.
18
+ *
19
+ * The icon components are react-icons functions, taken from `context.icons`:
20
+ * the build's tree-shaken icon artifact, the same map the client resolves names
21
+ * against, so a report can only draw icons the app actually bundles.
22
+ *
23
+ * React renders them: `react` and `react-dom/server` are imported lazily on
24
+ * first use, which keeps the static entry free of React at module load (every
25
+ * report imports this package for Box and Span, but only a page with an Icon
26
+ * pays for React). Serialising the element tree by hand was rejected because
27
+ * react-icons ships React-cased attributes (`fillRule`, `strokeLinecap`,
28
+ * `clipPath`; 457 of them in one icon family alone), so a second serialiser
29
+ * would have to carry its own copy of React's SVG attribute-casing table.
30
+ */ import { type } from '@lowdefy/helpers';
31
+ import { styleValue, toPoints } from '@lowdefy/block-utils/report';
32
+ // A line of body text is ~14pt, which is the size an icon beside a label wants.
33
+ const DEFAULT_SIZE = 14;
34
+ // react-icons paints with `currentColor`; the client inherits the surrounding
35
+ // text colour, and a document has none, so fall back to the body text colour.
36
+ const DEFAULT_COLOR = '#262626';
37
+ let renderer;
38
+ // react-dom/server resolves the React-cased attributes and the `currentColor`
39
+ // default the same way the browser does. Loaded once per process.
40
+ async function getRenderer() {
41
+ if (!renderer) {
42
+ const [{ createElement }, reactDomServer] = await Promise.all([
43
+ import('react'),
44
+ import('react-dom/server')
45
+ ]);
46
+ const { renderToStaticMarkup } = reactDomServer.default ?? reactDomServer;
47
+ renderer = {
48
+ createElement,
49
+ renderToStaticMarkup
50
+ };
51
+ }
52
+ return renderer;
53
+ }
54
+ const toReport = async ({ block, context })=>{
55
+ // `properties` may be the icon name alone (`type: Icon, properties: AiFillHome`),
56
+ // exactly as the client's createIcon reads it.
57
+ const properties = type.isString(block.properties) ? {
58
+ name: block.properties
59
+ } : block.properties ?? {};
60
+ const name = properties.name;
61
+ const Component = name ? context?.icons?.[name] : undefined;
62
+ if (!Component) {
63
+ // The page shows a fallback glyph for an unknown name; a document is better
64
+ // off without it, so skip and name the block instead.
65
+ context?.logger?.warn?.({
66
+ blockId: block.blockId,
67
+ name
68
+ }, `Icon block '${block.blockId}' names icon '${name}', which the app does not bundle. Skipped.`);
69
+ return null;
70
+ }
71
+ const size = toPoints(properties.size) ?? toPoints(styleValue(block.style, 'fontSize')) ?? DEFAULT_SIZE;
72
+ const rawColor = properties.color ?? styleValue(block.style, 'color') ?? DEFAULT_COLOR;
73
+ // The colour is spliced into SVG markup below, so reject any value carrying
74
+ // markup characters (a config-authored `red"/><image .../>`) and fall back to
75
+ // the default rather than let it break out of the attribute.
76
+ const color = /[<>"]/.test(String(rawColor)) ? DEFAULT_COLOR : rawColor;
77
+ if (!type.isUndefined(properties.rotate) || properties.spin === true) {
78
+ context?.logger?.debug?.({
79
+ blockId: block.blockId
80
+ }, `Report icons ignore rotate and spin on block '${block.blockId}'.`);
81
+ }
82
+ try {
83
+ const { createElement, renderToStaticMarkup } = await getRenderer();
84
+ const markup = renderToStaticMarkup(createElement(Component, {
85
+ size,
86
+ color,
87
+ title: properties.title
88
+ }));
89
+ // pdfmake's SVG renderer has no CSS cascade, so it cannot resolve the
90
+ // keyword react-icons paints with; bake the resolved colour in.
91
+ const svg = markup.split('currentColor').join(color);
92
+ return {
93
+ kind: 'svg',
94
+ svg,
95
+ width: size,
96
+ height: size
97
+ };
98
+ } catch (error) {
99
+ context?.logger?.warn?.({
100
+ blockId: block.blockId,
101
+ err: error
102
+ }, `Icon block '${block.blockId}' failed to render and was skipped: ${error.message}`);
103
+ return null;
104
+ }
105
+ };
106
+ export const Icon = {
107
+ toReport
108
+ };
@@ -14,6 +14,7 @@
14
14
  limitations under the License.
15
15
  */ export default {
16
16
  category: 'display',
17
+ static: true,
17
18
  icons: [
18
19
  'AiOutlineCloseCircle'
19
20
  ],
@@ -0,0 +1,39 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */ import { type } from '@lowdefy/helpers';
16
+ import { isBlank } from '@lowdefy/block-utils/report';
17
+ /**
18
+ * Img → `image`. `properties.src` names the source; the reports plugin resolves
19
+ * the bytes centrally (data URI, public asset, or guarded fetch). The block's
20
+ * `width`/`height` properties become the IR node's width/height, carried
21
+ * through as PostScript points, the same unit every other sizeable IR node
22
+ * uses. With neither set the image keeps its natural size, capped to the
23
+ * content width. An empty src yields no node.
24
+ */ export const Img = {
25
+ toReport: ({ block })=>{
26
+ const { src, width, height } = block.properties ?? {};
27
+ if (isBlank(src)) return null;
28
+ return {
29
+ kind: 'image',
30
+ src: String(src),
31
+ ...type.isNumber(width) ? {
32
+ width
33
+ } : {},
34
+ ...type.isNumber(height) ? {
35
+ height
36
+ } : {}
37
+ };
38
+ }
39
+ };
@@ -14,6 +14,7 @@
14
14
  limitations under the License.
15
15
  */ export default {
16
16
  category: 'display',
17
+ static: true,
17
18
  icons: [],
18
19
  valueType: null,
19
20
  cssKeys: {
@@ -0,0 +1,35 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */ import { htmlToText, isBlank } from '@lowdefy/block-utils/report';
16
+ /**
17
+ * Span → `text`. The Span block shows `properties.content` when set, otherwise
18
+ * its `content` area; the report mirrors that precedence. A content string
19
+ * becomes a `text` (markup flattened); otherwise the walked area is wrapped in
20
+ * a stack. A Span with neither yields no node.
21
+ */ export const Span = {
22
+ toReport: ({ block, areas = {} })=>{
23
+ const content = block.properties?.content;
24
+ if (!isBlank(content)) return {
25
+ kind: 'text',
26
+ text: htmlToText(content)
27
+ };
28
+ const nodes = areas.content ?? [];
29
+ if (nodes.length === 0) return null;
30
+ return {
31
+ kind: 'stack',
32
+ children: nodes
33
+ };
34
+ }
35
+ };
@@ -14,6 +14,7 @@
14
14
  limitations under the License.
15
15
  */ export default {
16
16
  category: 'container',
17
+ static: true,
17
18
  icons: [],
18
19
  valueType: null,
19
20
  slots: {
package/dist/static.js ADDED
@@ -0,0 +1,26 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */ /**
16
+ * Static report renderers for blocks-basic. Each export is keyed by block type
17
+ * and exposes `{ toReport }`, mapping a block's evaluated properties (and, for
18
+ * containers, its walked `areas`) to report IR. This entry must stay free of
19
+ * React at load time so the server can load the registry without a browser
20
+ * runtime; renderers emit plain IR object literals, never pdfmake or ExcelJS
21
+ * objects. (The Icon renderer imports React lazily, on the first icon it draws.)
22
+ */ export { Img } from './blocks/Img/Img.static.js';
23
+ export { Span } from './blocks/Span/Span.static.js';
24
+ export { Box } from './blocks/Box/Box.static.js';
25
+ export { Html, DangerousHtml } from './blocks/Html/Html.static.js';
26
+ export { Icon } from './blocks/Icon/Icon.static.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lowdefy/blocks-basic",
3
- "version": "0.0.0-experimental-20260908154018",
3
+ "version": "0.0.0-experimental-20260909123240",
4
4
  "license": "Apache-2.0",
5
5
  "description": "Basic html Lowdefy blocks.",
6
6
  "homepage": "https://lowdefy.com",
@@ -36,14 +36,15 @@
36
36
  "./blocks": "./dist/blocks.js",
37
37
  "./e2e": "./dist/e2e.js",
38
38
  "./metas": "./dist/metas.js",
39
+ "./static": "./dist/static.js",
39
40
  "./types": "./dist/types.js"
40
41
  },
41
42
  "files": [
42
43
  "dist/*"
43
44
  ],
44
45
  "dependencies": {
45
- "@lowdefy/block-utils": "0.0.0-experimental-20260908154018",
46
- "@lowdefy/helpers": "0.0.0-experimental-20260908154018",
46
+ "@lowdefy/block-utils": "0.0.0-experimental-20260909123240",
47
+ "@lowdefy/helpers": "0.0.0-experimental-20260909123240",
47
48
  "dompurify": "3.4.2"
48
49
  },
49
50
  "peerDependencies": {
@@ -51,12 +52,15 @@
51
52
  "react-dom": ">=18"
52
53
  },
53
54
  "devDependencies": {
54
- "@lowdefy/block-dev-e2e": "0.0.0-experimental-20260908154018",
55
- "@lowdefy/e2e-utils": "0.0.0-experimental-20260908154018",
55
+ "@jest/globals": "28.1.3",
56
+ "@lowdefy/block-dev-e2e": "0.0.0-experimental-20260909123240",
57
+ "@lowdefy/e2e-utils": "0.0.0-experimental-20260909123240",
56
58
  "@playwright/test": "1.59.1",
57
59
  "@swc/cli": "0.8.1",
58
60
  "@swc/core": "1.15.32",
59
- "copyfiles": "2.4.1"
61
+ "@swc/jest": "0.2.39",
62
+ "copyfiles": "2.4.1",
63
+ "jest": "28.1.3"
60
64
  },
61
65
  "publishConfig": {
62
66
  "access": "public"
@@ -66,6 +70,7 @@
66
70
  "clean": "rm -rf dist",
67
71
  "copyfiles": "copyfiles -u 1 \"./src/**/*\" dist -e \"./src/**/*.js\" -e \"./src/**/*.snap\"",
68
72
  "e2e": "playwright test --config e2e/playwright.config.js",
69
- "e2e:ui": "playwright test --config e2e/playwright.config.js --ui"
73
+ "e2e:ui": "playwright test --config e2e/playwright.config.js --ui",
74
+ "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js"
70
75
  }
71
76
  }