From 75ccefadf7bc842e78f65903bee4e29d79ab1156 Mon Sep 17 00:00:00 2001 From: Will Chen Date: Mon, 10 Jun 2024 14:21:53 -0700 Subject: [PATCH] Add HTML component (#237) --- demo/html_demo.py | 11 +++++ demo/main.py | 2 + docs/components/html.md | 17 ++++++++ mesop/BUILD | 1 + mesop/__init__.py | 1 + mesop/components/html/BUILD | 12 ++++++ mesop/components/html/__init__.py | 0 mesop/components/html/e2e/BUILD | 13 ++++++ mesop/components/html/e2e/__init__.py | 1 + mesop/components/html/e2e/html_app.py | 11 +++++ mesop/components/html/e2e/html_test.ts | 9 ++++ mesop/components/html/html.ng.html | 1 + mesop/components/html/html.proto | 7 ++++ mesop/components/html/html.py | 41 +++++++++++++++++++ mesop/components/html/html.ts | 34 +++++++++++++++ mesop/example_index.py | 1 + mesop/examples/BUILD | 1 + mesop/web/src/component_renderer/BUILD | 1 + .../component_renderer/type_to_component.ts | 2 + mkdocs.yml | 1 + 20 files changed, 167 insertions(+) create mode 100644 demo/html_demo.py create mode 100644 docs/components/html.md create mode 100644 mesop/components/html/BUILD create mode 100644 mesop/components/html/__init__.py create mode 100644 mesop/components/html/e2e/BUILD create mode 100644 mesop/components/html/e2e/__init__.py create mode 100644 mesop/components/html/e2e/html_app.py create mode 100644 mesop/components/html/e2e/html_test.ts create mode 100644 mesop/components/html/html.ng.html create mode 100644 mesop/components/html/html.proto create mode 100644 mesop/components/html/html.py create mode 100644 mesop/components/html/html.ts diff --git a/demo/html_demo.py b/demo/html_demo.py new file mode 100644 index 000000000..c3c2ae082 --- /dev/null +++ b/demo/html_demo.py @@ -0,0 +1,11 @@ +import mesop as me + + +@me.page(path="/html_demo") +def app(): + me.html( + """ +Custom HTML +mesop +""" + ) diff --git a/demo/main.py b/demo/main.py index 91240b6a1..d73db1d27 100644 --- a/demo/main.py +++ b/demo/main.py @@ -30,6 +30,7 @@ import code_demo as code_demo # cannot call it code due to python library naming conflict import divider as divider import embed as embed +import html_demo as html_demo import icon as icon import image as image import input as input @@ -146,6 +147,7 @@ class Section: name="Advanced", examples=[ Example(name="embed"), + Example(name="html_demo"), Example(name="plot"), ], ), diff --git a/docs/components/html.md b/docs/components/html.md new file mode 100644 index 000000000..6d70f8b42 --- /dev/null +++ b/docs/components/html.md @@ -0,0 +1,17 @@ +## Overview + +The HTML component allows you to add custom HTML to your Mesop app. + +> Note: the HTML is [sanitized by Angular](https://angular.dev/best-practices/security#sanitization-example) for web security reasons so potentially unsafe code like JavaScript is removed. + +## Examples + + + +```python +--8<-- "demo/html_demo.py" +``` + +## API + +::: mesop.components.html.html.html diff --git a/mesop/BUILD b/mesop/BUILD index 0c3b9d1b3..360346e9b 100644 --- a/mesop/BUILD +++ b/mesop/BUILD @@ -19,6 +19,7 @@ py_library( deps = [ ":version", # REF(//scripts/scaffold_component.py):insert_component_import + "//mesop/components/html:py", "//mesop/components/uploader:py", "//mesop/components/code:py", "//mesop/components/embed:py", diff --git a/mesop/__init__.py b/mesop/__init__.py index 0ad5e6594..fbff595f3 100644 --- a/mesop/__init__.py +++ b/mesop/__init__.py @@ -56,6 +56,7 @@ from mesop.components.code.code import code as code from mesop.components.divider.divider import divider as divider from mesop.components.embed.embed import embed as embed +from mesop.components.html.html import html as html from mesop.components.icon.icon import icon as icon from mesop.components.image.image import image as image from mesop.components.input.input import EnterEvent as EnterEvent diff --git a/mesop/components/html/BUILD b/mesop/components/html/BUILD new file mode 100644 index 000000000..a028007ab --- /dev/null +++ b/mesop/components/html/BUILD @@ -0,0 +1,12 @@ +load("//mesop/components:defs.bzl", "mesop_component") + +package( + default_visibility = ["//build_defs:mesop_internal"], +) + +mesop_component( + name = "html", + ng_deps = [ + "//mesop/web/src/safe_iframe", + ], +) diff --git a/mesop/components/html/__init__.py b/mesop/components/html/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/mesop/components/html/e2e/BUILD b/mesop/components/html/e2e/BUILD new file mode 100644 index 000000000..f77d6e0f5 --- /dev/null +++ b/mesop/components/html/e2e/BUILD @@ -0,0 +1,13 @@ +load("//build_defs:defaults.bzl", "py_library") + +package( + default_visibility = ["//build_defs:mesop_examples"], +) + +py_library( + name = "e2e", + srcs = glob(["*.py"]), + deps = [ + "//mesop", + ], +) diff --git a/mesop/components/html/e2e/__init__.py b/mesop/components/html/e2e/__init__.py new file mode 100644 index 000000000..18b37d7d7 --- /dev/null +++ b/mesop/components/html/e2e/__init__.py @@ -0,0 +1 @@ +from . import html_app as html_app diff --git a/mesop/components/html/e2e/html_app.py b/mesop/components/html/e2e/html_app.py new file mode 100644 index 000000000..c9b4f95af --- /dev/null +++ b/mesop/components/html/e2e/html_app.py @@ -0,0 +1,11 @@ +import mesop as me + + +@me.page(path="/components/html/e2e/html_app") +def app(): + me.html( + """ +Custom HTML +mesoplink +""" + ) diff --git a/mesop/components/html/e2e/html_test.ts b/mesop/components/html/e2e/html_test.ts new file mode 100644 index 000000000..2be4c8524 --- /dev/null +++ b/mesop/components/html/e2e/html_test.ts @@ -0,0 +1,9 @@ +import {test, expect} from '@playwright/test'; + +test('test', async ({page}) => { + await page.goto('/components/html/e2e/html_app'); + // mesop is the HTML link so we're checking that it's rendered. + expect(await page.getByText('Custom HTML').textContent()).toContain( + 'mesoplink', + ); +}); diff --git a/mesop/components/html/html.ng.html b/mesop/components/html/html.ng.html new file mode 100644 index 000000000..28d4b24ac --- /dev/null +++ b/mesop/components/html/html.ng.html @@ -0,0 +1 @@ +
diff --git a/mesop/components/html/html.proto b/mesop/components/html/html.proto new file mode 100644 index 000000000..fd707c19b --- /dev/null +++ b/mesop/components/html/html.proto @@ -0,0 +1,7 @@ +syntax = "proto2"; + +package mesop.components.html; + +message HtmlType { + optional string html = 1; +} diff --git a/mesop/components/html/html.py b/mesop/components/html/html.py new file mode 100644 index 000000000..e59411cd1 --- /dev/null +++ b/mesop/components/html/html.py @@ -0,0 +1,41 @@ +import mesop.components.html.html_pb2 as html_pb +from mesop.component_helpers import ( + Border, + BorderSide, + Style, + insert_component, + register_native_component, +) + + +@register_native_component +def html( + html: str = "", + *, + style: Style | None = None, + key: str | None = None, +): + """ + This function renders custom HTML inside an iframe for web security isolation. + + Args: + html: The HTML content to be rendered. + style: The style to apply to the embed, such as width and height. + key: The component [key](../guides/components.md#component-key). + """ + if style is None: + style = Style() + if style.border is None: + style.border = Border.all( + BorderSide( + width=0, + ) + ) + insert_component( + key=key, + type_name="html", + proto=html_pb.HtmlType( + html=html, + ), + style=style, + ) diff --git a/mesop/components/html/html.ts b/mesop/components/html/html.ts new file mode 100644 index 000000000..ed4323b00 --- /dev/null +++ b/mesop/components/html/html.ts @@ -0,0 +1,34 @@ +import {Component, Input} from '@angular/core'; +import { + Key, + Style, + Type, +} from 'mesop/mesop/protos/ui_jspb_proto_pb/mesop/protos/ui_pb'; +import {HtmlType} from 'mesop/mesop/components/html/html_jspb_proto_pb/mesop/components/html/html_pb'; +import {formatStyle} from '../../web/src/utils/styles'; + +@Component({ + selector: 'mesop-html', + templateUrl: 'html.ng.html', + standalone: true, +}) +export class HtmlComponent { + @Input({required: true}) type!: Type; + @Input() key!: Key; + @Input() style!: Style; + private _config!: HtmlType; + + ngOnChanges() { + this._config = HtmlType.deserializeBinary( + this.type.getValue() as unknown as Uint8Array, + ); + } + + config(): HtmlType { + return this._config; + } + + getStyle(): string { + return formatStyle(this.style); + } +} diff --git a/mesop/example_index.py b/mesop/example_index.py index f75a1f6de..8e237d3cf 100644 --- a/mesop/example_index.py +++ b/mesop/example_index.py @@ -28,4 +28,5 @@ import mesop.components.table.e2e as table_e2e import mesop.components.embed.e2e as embed_e2e import mesop.components.uploader.e2e as uploader_e2e +import mesop.components.html.e2e as html_e2e # REF(//scripts/scaffold_component.py):insert_component_e2e_import_export diff --git a/mesop/examples/BUILD b/mesop/examples/BUILD index 1d3fd24ba..89474abfc 100644 --- a/mesop/examples/BUILD +++ b/mesop/examples/BUILD @@ -15,6 +15,7 @@ py_library( deps = [ "//demo", # REF(//scripts/scaffold_component.py):insert_component_e2e_import + "//mesop/components/html/e2e", "//mesop/components/uploader/e2e", "//mesop/components/embed/e2e", "//mesop/components/table/e2e", diff --git a/mesop/web/src/component_renderer/BUILD b/mesop/web/src/component_renderer/BUILD index 12c197e7a..20288825f 100644 --- a/mesop/web/src/component_renderer/BUILD +++ b/mesop/web/src/component_renderer/BUILD @@ -14,6 +14,7 @@ ng_module( ]) + ["component_renderer.css"], deps = [ # REF(//scripts/scaffold_component.py):insert_component_import + "//mesop/components/html:ng", "//mesop/components/uploader:ng", "//mesop/components/embed:ng", "//mesop/components/table:ng", diff --git a/mesop/web/src/component_renderer/type_to_component.ts b/mesop/web/src/component_renderer/type_to_component.ts index a758d03c5..0b2dbe763 100644 --- a/mesop/web/src/component_renderer/type_to_component.ts +++ b/mesop/web/src/component_renderer/type_to_component.ts @@ -1,3 +1,4 @@ +import {HtmlComponent} from '../../../components/html/html'; import {UploaderComponent} from '../../../components/uploader/uploader'; import {EmbedComponent} from '../../../components/embed/embed'; import {TableComponent} from '../../../components/table/table'; @@ -53,6 +54,7 @@ export class UserDefinedComponent implements BaseComponent { } export const typeToComponent = { + 'html': HtmlComponent, 'uploader': UploaderComponent, 'embed': EmbedComponent, 'table': TableComponent, diff --git a/mkdocs.yml b/mkdocs.yml index f8cd5bd54..362044324 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -51,6 +51,7 @@ nav: - Tooltip: components/tooltip.md - Advanced: - Embed: components/embed.md + - HTML: components/html.md - Plot: components/plot.md - API: - Page: api/page.md