Skip to content

Commit

Permalink
Add HTML component
Browse files Browse the repository at this point in the history
  • Loading branch information
wwwillchen committed Jun 10, 2024
1 parent 2b24618 commit 71711c4
Show file tree
Hide file tree
Showing 21 changed files with 210 additions and 0 deletions.
11 changes: 11 additions & 0 deletions demo/html_demo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import mesop as me


@me.page(path="/html_demo")
def app():
me.html(
"""
Custom HTML
<a href="https://google.github.io/mesop/" target="_blank">mesop</a>
"""
)
2 changes: 2 additions & 0 deletions demo/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -146,6 +147,7 @@ class Section:
name="Advanced",
examples=[
Example(name="embed"),
Example(name="html_demo"),
Example(name="plot"),
],
),
Expand Down
17 changes: 17 additions & 0 deletions docs/components/html.md
Original file line number Diff line number Diff line change
@@ -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

<iframe class="component-demo" src="https://mesop-y677hytkra-uc.a.run.app/html"></iframe>

```python
--8<-- "demo/html_demo.py"
```

## API

::: mesop.components.html.html.html
1 change: 1 addition & 0 deletions mesop/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions mesop/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 12 additions & 0 deletions mesop/components/html/BUILD
Original file line number Diff line number Diff line change
@@ -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",
],
)
Empty file.
13 changes: 13 additions & 0 deletions mesop/components/html/e2e/BUILD
Original file line number Diff line number Diff line change
@@ -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",
],
)
1 change: 1 addition & 0 deletions mesop/components/html/e2e/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import html_app as html_app
11 changes: 11 additions & 0 deletions mesop/components/html/e2e/html_app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import mesop as me


@me.page(path="/components/html/e2e/html_app")
def app():
me.html(
"""
Custom HTML
<a href="https://google.github.io/mesop/" target="_blank">mesoplink</a>
"""
)
9 changes: 9 additions & 0 deletions mesop/components/html/e2e/html_test.ts
Original file line number Diff line number Diff line change
@@ -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',
);
});
1 change: 1 addition & 0 deletions mesop/components/html/html.ng.html
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<div [innerHTML]="config().getHtml()" [style]="getStyle()"></div>
7 changes: 7 additions & 0 deletions mesop/components/html/html.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
syntax = "proto2";

package mesop.components.html;

message HtmlType {
optional string html = 1;
}
41 changes: 41 additions & 0 deletions mesop/components/html/html.py
Original file line number Diff line number Diff line change
@@ -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,
)
51 changes: 51 additions & 0 deletions mesop/components/html/html.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import {Component, ElementRef, Input, ViewChild} 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;
@ViewChild('iframe', {read: ElementRef}) iframe!: ElementRef;
private _config!: HtmlType;
private srcDoc!: string;

ngOnChanges() {
this._config = HtmlType.deserializeBinary(
this.type.getValue() as unknown as Uint8Array,
);
// const previousSrcDoc = this.srcDoc;
// this.srcDoc = this._config.getHtml()!;

// // Reload iframe if the URL has changed.
// if (
// this.srcDoc !== previousSrcDoc &&
// this.iframe &&
// this.iframe.nativeElement
// ) {
// this.loadIframe();
// }
}

ngAfterViewInit() {
// this.loadIframe();
}

config(): HtmlType {
return this._config;
}

getStyle(): string {
return formatStyle(this.style);
}
}
1 change: 1 addition & 0 deletions mesop/example_index.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
1 change: 1 addition & 0 deletions mesop/examples/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions mesop/web/src/component_renderer/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 2 additions & 0 deletions mesop/web/src/component_renderer/type_to_component.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -53,6 +54,7 @@ export class UserDefinedComponent implements BaseComponent {
}

export const typeToComponent = {
'html': HtmlComponent,
'uploader': UploaderComponent,
'embed': EmbedComponent,
'table': TableComponent,
Expand Down
26 changes: 26 additions & 0 deletions mesop/web/src/safe_iframe/safe_iframe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ export function setIframeSrc(iframe: HTMLIFrameElement, src: string) {
setIframeSrcImpl(iframe, src);
}

export function setIframeSrcDoc(iframe: HTMLIFrameElement, srcDoc: string) {
// Intentionally delegate to an impl function because the following
// line will be modified downstream.
setIframeSrcDocImpl(iframe, srcDoc);
}

// copybara:strip_begin(external-only)
function setIframeSrcImpl(iframe: HTMLIFrameElement, src: string) {
// This is a tightly controlled list of attributes that enables us to
Expand All @@ -30,4 +36,24 @@ function setIframeSrcImpl(iframe: HTMLIFrameElement, src: string) {

iframe.src = sanitizeJavaScriptUrl(src)!;
}

function setIframeSrcDocImpl(iframe: HTMLIFrameElement, srcdoc: string) {
// This is a tightly controlled list of attributes that enables us to
// secure sandbox iframes. Do not add additional attributes without
// consulting a security resource.
//
// Ref:
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe#sandbox
iframe.sandbox.add(
'allow-same-origin',
'allow-scripts',
'allow-forms',
'allow-popups',
'allow-popups-to-escape-sandbox',
'allow-storage-access-by-user-activation',
);

// Check if there's any santiziation that's needed.
iframe.srcdoc = srcdoc;
}
// copybara:strip_end
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 71711c4

Please sign in to comment.