Skip to content

Commit

Permalink
Add HTML component
Browse files Browse the repository at this point in the history
  • Loading branch information
wwwillchen committed May 11, 2024
1 parent ddb5723 commit d14c901
Show file tree
Hide file tree
Showing 20 changed files with 214 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 @@ -28,6 +28,7 @@
import checkbox as checkbox
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 @@ -133,6 +134,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 rendered inside an iframe for web security reasons so it cannot interact with other Mesop components.
## Examples

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

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

## API

::: mesop.components.link.link.link
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 @@ -55,6 +55,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 input as input
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
12 changes: 12 additions & 0 deletions mesop/components/html/e2e/html_app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
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">mesop</a>
"""
)
me.text("Text after the custom HTML")
8 changes: 8 additions & 0 deletions mesop/components/html/e2e/html_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import {test, expect} from '@playwright/test';

test('test', async ({page}) => {
await page.goto('/components/html/e2e/html_app');
expect(await page.getByText('Hello, world!').textContent()).toContain(
'Hello, world!',
);
});
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 @@
<iframe #iframe [style]="getStyle()"></iframe>
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,
)
56 changes: 56 additions & 0 deletions mesop/components/html/html.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
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';
import {setIframeSrcDoc} from '../../web/src/safe_iframe/safe_iframe';

@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();
}

loadIframe() {
setIframeSrcDoc(this.iframe.nativeElement, this.srcDoc);
}

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

0 comments on commit d14c901

Please sign in to comment.