Repository: pyscript/pyscript

Issue:
[BUG] print() doesn't output HTML tags.
**print() doesn't output HTML tags**
I was experimenting in Pyscript and I tried to print an HTML table, but it didn't work. It seems to delete the tags and mantain just the plain text.

This is the code that I tried, but it just printed "test" once:

```HTML
<py-script>
print("<table>")
for i in range (2):
    print("<tr>")
    for j in range (2):
        print("<td>test</td>")
    print("</tr>")
print("</table>")
</py-script>
```

And this is a screenshot I took of the output:
![image](https://user-images.githubusercontent.com/76500649/168032565-76d0bdf3-7570-4916-8e1b-54b0853f9c3d.png)



Discussion:
...use the print function in line to generate the HTML....well that's interesting. 👌 
@pnhearer I don't get if that's ironic or not. If so, why is generating HTML with scripting languages wrong? :(
@volpoh Please try and print the entire html in a single print call, as you see that each print call essentially packs the python code inside a set of `<div></div>`.

An option is to create a list of all the line-strings and then join that list to create the body of the table as follows. 

```python
<py-script>
from typing import List

test = [
    ["R1C1", "R1C2"], 
    ["R2C1", "R2C2"],
]


html: List[str] = []
html.append("<table>")
for i in range(2):
    html.append("\t<tr>")
    for j in range(2):
        html.append(f"\t\t<td>{test[i][j]}</td>")
   html.append("\t</tr>")
html.append("</table>")

print("\n".join(html))
</py-script>
```

Alternatively, consider using Jinja templating, which is quite standard for such programmatic generation of code-body.
@sugatoray **I tried and this was the output:**

![image](https://user-images.githubusercontent.com/76500649/168122303-9297ddf3-a7d5-4bb0-80da-49b44a3e068f.png)

That's not a problem because I can always use JS, TS or something else... I was just curious about why the pyscript print function doesn't allow to generate HTML output.
I hope that this Issue can be useful, if not I'll close it. Let me know, and thank you so much for your answer! Have a nice day.
@fpliger 
Problem makes 

```javascript
    function htmlDecode(input) {
        const doc = new DOMParser().parseFromString(ltrim(input), 'text/html');
        return doc.documentElement.textContent;
    }
```

(in file `utils.ts`) which removes all HTML tags from code in `<py-script>`

https://github.com/pyscript/pyscript/blob/main/pyscriptjs/src/utils.ts#L17

Probably it removes it in `components/pyscript.ts`

```javascript
    getSourceFromElement(): string {
        return htmlDecode(this.code);
    }
```

https://github.com/pyscript/pyscript/blob/main/pyscriptjs/src/components/pyscript.ts#L125

But probably the same problem can be when it loads code from file.

---

**How I recognized problem:**

If I run ie. 

```python
pyscript.write(some_id, "<h1>TEST</h1>")
```
then in console I can see

```
APPENDING: True ==> ... --> TEST
```
instead of 

```
APPENDING: True ==> ... --> <h1>TEST</h1>
```

And code 

```python
print(len("<h1>TEST</h1>"))
```

shows value `4` instead of `13`

---

**How I confirmed problem:**

I can see correct elements if I use 

```javascript
    function htmlDecode(input) {
        return input;
    }
```

---

**By The Way:**

For this example with `<h1>` problem makes also `pyscript.css` which redefines `<h1>` and this element looks like normal text - so for long time I couldn't figure out why it didn't want to render `<h1>`

---

Full HTML which I used for tests.

Problem was with `https://pyscript.net/alpha/pyscript.js` and solution I tested using local `pyscript.js` which I generated using `node.js` like in [documentation](https://github.com/pyscript/pyscript/tree/main/pyscriptjs). 

I also skiped `css`.

```html
<!DOCTYPE html>

<html lang="en">
  <head>
    <meta charset="utf-8" />

    <title>PyScript Hello World</title>

    <link rel="icon" type="image/png" href="favicon.png" />
    <!--<link rel="stylesheet" href="https://pyscript.net/alpha/pyscript.css" />-->
    <!--<script defer src="https://pyscript.net/alpha/pyscript.js"></script>-->

    <script defer src="/build/pyscript.js"></script>
  </head>
  
  <body>

    <div id="out1"></div>
    <div id="out2"></div>
    <div id="out3"></div>
    <div id="out4"></div>
    
    <py-script>

print("<h1>TEST 0</h1>")

element1 = document.createElement('div')
element1.innerHTML = "<h1>TEST 1</h1>"
document.getElementById("out1").append(element1)

element2 = document.createElement('div')
element2.innerHTML = "<h1>TEST 2</h1>"
document.getElementById("out2").appendChild(element2)

document.getElementById("out3").innerHTML = "<h1>TEST 3</h1>"

pyscript.write('out4', "<h1>TEST 4</h1>")

    </py-script>
  </body>
</html>
```
---

Tested on Linux Mint 20

browsers:
- Firefox 100.0
- Chrome 101.0.4951.64
- Brave 1.38.119 (engine Chromium: 101.0.4951.67)
- Microsoft Edge 103.0.1255.0
Some workaround is to use some replacement for tags in text - ie. use `{{ }}` instead of `< >` and later replace it back with `< >`

```python
print( "{{b}}Hello World{{/b}}".replace("{{", "<").replace("}}", ">") )
print( "{{h1}}Hello World{{/h1}}".replace("{{", "<").replace("}}", ">") )
```

or more universal - using function for this

```python
def HTML(text):
     return text.replace("{{", "<").replace("}}", ">")

print( HTML("{{b}}Hello World{{/b}}") )
print( HTML("{{h1}}Hello World{{/h1}}) )

pyscript.write(some_id, HTML("{{h1}}Hello World{{/h1}}) )

document.getElementById(some_id).innerHTML = HTML("{{h1}}Hello World{{/h1}})
```
The "right" or "wrong" to this will always depend on what the use case is. Sometimes you'll want escaped output and sometimes you won't. 

We just merged #439 which is slightly related to the topic, and [here's a PR](https://github.com/pyscript/pyscript/pull/121) that relates to the topic as well but ultimately we need to design a way for users to opt-in/out escaping . At least at the element (`<py-script>`, `<py-repl>`) level).

I'm in favor of trying to address this sooner rather than later.

/cc @philippjfr 
adding a note that maybe this issue is also related to the discussion in https://github.com/pyscript/pyscript/issues/373
This is related with this PR too... https://github.com/pyscript/pyscript/pull/481
This PR sanitizes everything.
If anyone would like to tackle the escaping version for this and design the API for it, it'd be cool! :)

Resolving patch:
diff --git a/pyscriptjs/src/python/pyscript.py b/pyscriptjs/src/python/pyscript.py
--- a/pyscriptjs/src/python/pyscript.py
+++ b/pyscriptjs/src/python/pyscript.py
@@ -1,5 +1,6 @@
 import asyncio
 import base64
+import html
 import io
 import time
 from textwrap import dedent
@@ -35,7 +36,7 @@ def identity(value, meta):
 
 
 MIME_RENDERERS = {
-    "text/plain": identity,
+    "text/plain": html.escape,
     "text/html": identity,
     "image/png": lambda value, meta: render_image("image/png", value, meta),
     "image/jpeg": lambda value, meta: render_image("image/jpeg", value, meta),
@@ -45,6 +46,18 @@ def identity(value, meta):
 }
 
 
+class HTML:
+    """
+    Wrap a string so that display() can render it as plain HTML
+    """
+
+    def __init__(self, html):
+        self._html = html
+
+    def _repr_html_(self):
+        return self._html
+
+
 def eval_formatter(obj, print_method):
     """
     Evaluates a formatter method.
@@ -68,7 +81,7 @@ def format_mime(obj):
     Formats object using _repr_x_ methods.
     """
     if isinstance(obj, str):
-        return obj, "text/plain"
+        return html.escape(obj), "text/plain"
 
     mimebundle = eval_formatter(obj, "_repr_mimebundle_")
     if isinstance(mimebundle, tuple):


Test patch:
diff --git a/pyscriptjs/tests/integration/test_02_output.py b/pyscriptjs/tests/integration/test_02_output.py
--- a/pyscriptjs/tests/integration/test_02_output.py
+++ b/pyscriptjs/tests/integration/test_02_output.py
@@ -1,3 +1,4 @@
+import html
 import re
 
 import pytest
@@ -201,6 +202,30 @@ def test_display_list_dict_tuple(self):
             == "['A', 1, '!']\n{'B': 2, 'List': ['A', 1, '!']}\n('C', 3, '!')"
         )
 
+    def test_display_should_escape(self):
+        self.pyscript_run(
+            """
+            <py-script>
+                display("<p>hello world</p>")
+            </py-script>
+            """
+        )
+        out = self.page.locator("py-script > div")
+        assert out.inner_html() == html.escape("<p>hello world</p>")
+        assert out.inner_text() == "<p>hello world</p>"
+
+    def test_display_HTML(self):
+        self.pyscript_run(
+            """
+            <py-script>
+                display(HTML("<p>hello world</p>"))
+            </py-script>
+            """
+        )
+        out = self.page.locator("py-script > div")
+        assert out.inner_html() == "<p>hello world</p>"
+        assert out.inner_text() == "hello world"
+
     def test_image_display(self):
         self.pyscript_run(
             """
diff --git a/pyscriptjs/tests/py-unit/test_pyscript.py b/pyscriptjs/tests/py-unit/test_pyscript.py
--- a/pyscriptjs/tests/py-unit/test_pyscript.py
+++ b/pyscriptjs/tests/py-unit/test_pyscript.py
@@ -1,3 +1,4 @@
+import sys
 from unittest.mock import Mock
 
 import pyscript
@@ -24,7 +25,26 @@ def test_element(self, monkeypatch):
 
 def test_format_mime_str():
     obj = "just a string"
+    out, mime = pyscript.format_mime(obj)
+    assert out == obj
+    assert mime == "text/plain"
 
-    res = pyscript.format_mime(obj)
-    assert res[0] == obj
-    assert res[1] == "text/plain"
+
+def test_format_mime_str_escaping():
+    obj = "<p>hello</p>"
+    out, mime = pyscript.format_mime(obj)
+    assert out == "&lt;p&gt;hello&lt;/p&gt;"
+    assert mime == "text/plain"
+
+
+def test_format_mime_repr_escaping():
+    out, mime = pyscript.format_mime(sys)
+    assert out == "&lt;module 'sys' (built-in)&gt;"
+    assert mime == "text/plain"
+
+
+def test_format_mime_HTML():
+    obj = pyscript.HTML("<p>hello</p>")
+    out, mime = pyscript.format_mime(obj)
+    assert out == "<p>hello</p>"
+    assert mime == "text/html"