brython-dev/brython

creation of a new web worker fails if the script is in a VFS and is set using the `src` attribute

Opened this issue · 0 comments

Hello,

the creation of a new web worker fails if the script is in a VFS and is set using the src attribute.

If I change the following line from:

s.src = file

to

s.innerText = open(file, "r").read()

then at least the web worker can be started.

If the line worker_script_path = "py/elephas/web_workers/formatter.py" is changed to worker_script_path = "formatter0.py", then the worker works as desired. In contrast to formatter.py, formatter0.py is not Base64 encoded and is not located in the VFS. However, both are identical in terms of functionality.

Thanks in advance for your help!

Error:

Traceback (most recent call last):
  File "brython_issue_20240917_1.html#webworker", line 105, in onload
    self._workers.append(WWPoolWorker(wid, self))
                         ^^^^^^^^^^^^^^^^^^^^^^^
  File "brython_issue_20240622.html#webworker", line 52, in __init__
    worker.create_worker(wid, self.onready, self.onmessage, self.onerror)
RuntimeError: No webworker with id 'webworker_26abd947_63b6_4d46_9e26_ee13fd35c4ef'

brython_issue_20240917_1.html

<!DOCTYPE html>
<html>
    <head>
        <!-- Required meta tags-->
        <meta charset="utf-8">
        <title>Brython - Issue Demo 20240917</title>
        <!-- Brython -->
        <script src="https://raw.githack.com/brython-dev/brython/master/www/src/brython.js"></script>
        <script src="https://raw.githack.com/brython-dev/brython/master/www/src/brython_stdlib.js"></script>
        <!-- <script type="text/python" class="webworker" id="formatter" src="formatter.py"></script>-->
        <script src="stermi.vfs.js"></script>
        <script type="text/python" id="webworker">
import uuid
from browser import document, console, worker, window


class WebWorker:
    _busy: bool = False
    _rdy: bool = False
    _msgs: list = None
    _w = None
    def __init__(self, src):
        self._msgs = []
        s = document.createElement("script")
        s.setAttribute("type", "text/python")
        s.className = "webworker"
        s.id = "webworker_" + uuid.uuid4()
        document.head.appendChild(s)
        worker.create_worker(s.id, self.onready, self.onmessage, self.onerror)

    def onready(self, worker):
        self._w = worker
        self._rdy = True
        if len(self._msgs) > 0:
            for msg in self._msgs:
                self._w.send(msg)

    def send(self, msg):
        self._busy = True
        if not self._rdy:
            console.debug("worker not yet ready...")
            self._msgs.append(msg)
        else:
            console.debug("sending message to worker...")
            self._w.send(msg)

    def onmessage(self, evt):
        pass

    def onerror(self, evt):
        pass


class WWPoolWorker(WebWorker):
    def __init__(self, wid, pool_leader):
        self._pool_leader = pool_leader
        self._msgs = []
        self._id = wid
        element = document.getElementById(wid)
        console.debug("element:", element)
        src = element.src
        console.debug("src:", src)
        console.debug("creating new worker...")
        worker.create_worker(wid, self.onready, self.onmessage, self.onerror)

    def onmessage(self, evt):
        self._busy = False
        self._pool_leader.onmessage(self, self._rid, evt)

    def send(self, msg, rid):
        console.debug("new message:", msg, rid)
        self._busy = True
        self._rid = rid
        self._w.send(msg)

    def onready(self, worker):
        super().onready(worker)
        self._pool_leader.send_pending_msgs()

    def onerror(self, error_msg):
        super().onready(worker)
        console.error(self._id, error_msg)


class WWPool:
    _msgs: list = None
    _promises: dict = None
    def __init__(self, src=None, file=None, wid=None, worker_count=5):
        self._msgs = []
        self._promises = {}
        self._workers = []
        if wid is not None:
            self._id = wid
            for i in range(worker_count):
                console.debug(f"starting new pool worker {wid}...")
                self._workers.append(WWPoolWorker(wid, self))
        elif file is not None or src is not None:
            s = document.createElement("script")
            s.className = "webworker"
            new_uuid = str(uuid.uuid4()).replace("-", "_")
            wid = s.id = "webworker_" + new_uuid
            s.setAttribute("type", "text/python")
            """
            def observe(records, obs):
                if records[0].addedNodes[0].id == wid:
                    for i in range(worker_count):
                        console.debug("starting new pool worker...")
                        self._workers.append(WWPoolWorker(wid, self))
                    observer.disconnect()
            observer = window.MutationObserver.new(observe)
            observer.observe(document.body, {'childList': True})
            """
            def onload(ev):
                console.debug("web worker onload:", ev)
                for i in range(worker_count):
                    console.debug(f"starting new pool worker {wid}...")
                    self._workers.append(WWPoolWorker(wid, self))
                s.removeEventListener("load", onload)
            s.addEventListener("load", onload)
            if src is not None:
                console.debug(f"setting src of worker {wid}...")
                s.innerText = src
            elif file is not None:
                # does currently not work, see #2215 for more informations
                console.debug(f"setting target script of worker {wid} to {file}...")
                s.src = file
                #s.innerText = open(file, "r").read()
            #document.body.appendChild(s)
            document <= s
        else:
            pass
        #if wid is not None:
        #    for i in range(worker_count):
        #        console.debug("starting new pool worker...")
        #        self._workers.append(WWPoolWorker(wid, self))
    
    def send(self, msg):
        new_uuid = str(uuid.uuid4())
        def promise_func(resolve, reject):
            console.debug("new promise:", new_uuid)
            self._promises[new_uuid] = resolve
            for worker in self._workers:
                if not worker._busy and worker._rdy:
                    console.debug(f"sending message {new_uuid} to worker:", worker)
                    worker.send(msg, new_uuid)
                    break
            else:
                console.debug(f"no worker available! adding message {new_uuid} to queue...")
                self._msgs.append((new_uuid, msg))
        new_promise = window.Promise.new(promise_func)
        return new_promise
    
    def send_pending_msgs(self):
        console.debug("pending messages:", len(self._msgs), self._msgs)
        for worker in self._workers:
            if len(self._msgs) == 0:
                console.debug("message queue is empty! aborting...")
                return
            if not worker._busy and worker._rdy:
                new_uuid, msg = self._msgs.pop(0)
                console.debug(f"sending pending message {new_uuid} to worker:", worker)
                worker.send(msg, new_uuid)


    def onmessage(self, woker, rid, evt):
        console.debug("received message from worker:", rid, evt, evt.data)
        console.debug("promises:", self._promises)
        promise = self._promises[rid]
        console.debug("promise:", promise)
        promise(evt.data)
        del self._promises[rid]
        self.send_pending_msgs()

        </script>
        <script type="text/python" id="formatter_lib">
# Standard library imports.
import os
from browser import console, window
import webworker as ww

# Related third party imports.

# Local application/library specific imports.

console.debug("creating webworker pool...")
worker_script_path = "py/elephas/web_workers/formatter.py"
#worker_script_path = "formatter0.py"

with open(worker_script_path) as fh:
    console.debug(fh.read())

wwpool = ww.WWPool(file=worker_script_path)
console.debug("Done!")


def ww_format(string, **data):
    console.debug("formatting string:", string, data)
    formatted_str = wwpool.send([string, data])
    console.debug("promise:", formatted_str)
    return formatted_str
        </script>
    </head>
    <body onload="brython({debug: 2});">
<script type="text/python">
import json
import formatter_lib
from browser import console, aio, document


async def format():
    mapping = json.loads(document["mapping"].value)
    ta_in = document["in"]
    ta_out = document["out"]
    ta_out.value = await formatter_lib.ww_format(ta_in.value, **mapping)


def btn_format_click(evt):
    aio.run(format())


document["btn_format"].addEventListener("click", btn_format_click)


def btn_ww_info_click(evt):
    console.debug(formatter_lib.wwpool._msgs)
    for worker in formatter_lib.wwpool._workers:
        console.debug(worker._rdy, worker._busy)


document["btn_ww_info"].addEventListener("click", btn_ww_info_click)
        </script>
        <textarea id="mapping">{"test": "test123"}</textarea>
        <textarea id="in">test %test% \%test\%</textarea>
        <textarea id="out"></textarea>
        <div id="container"></div>
        <button id="btn_format">Format</button>
        <button id="btn_ww_info">WW Info</button>
    </body>
</html>

stermi.vfs.js

__BRYTHON__.use_VFS = true;
__BRYTHON__.add_files({
    "py/elephas/web_workers/formatter.py": {
        "content": "IyEvdXNyL2Jpbi9lbnYgcHl0aG9uMwoKIyBTdGFuZGFyZCBsaWJyYXJ5IGltcG9ydHMuCmZyb20gY29sbGVjdGlvbnMgaW1wb3J0IGRlcXVlCiNmcm9tIGJyb3dzZXIud2Vid29ya2VyIGltcG9ydCBjdXJyZW50X3dvcmtlciwgTWVzc2FnZQpmcm9tIHN5cyBpbXBvcnQgYXJndgpmcm9tIG9zIGltcG9ydCBlbnZpcm9uCmltcG9ydCBodG1sCiMgSW4gd2ViIHdvcmtlcnMsICJ3aW5kb3ciIGlzIHJlcGxhY2VkIGJ5ICJzZWxmIi4KZnJvbSBicm93c2VyIGltcG9ydCBiaW5kLCBzZWxmLCBjb25zb2xlCmltcG9ydCBqc29uCgojIFJlbGF0ZWQgdGhpcmQgcGFydHkgaW1wb3J0cy4KCiMgTG9jYWwgYXBwbGljYXRpb24vbGlicmFyeSBzcGVjaWZpYyBpbXBvcnRzLgojZnJvbSBzdGVybWkgaW1wb3J0IGZvcm1hdHRlcgoKY29uc29sZS5kZWJ1Zygic3RhcnRlZCBmb3JtYXQgd2ViIHdvcmtlciIpCgoKZGVmIGZvcm1hdChzdHJpbmc6IHN0ciwgZGF0YSk6CiAgICBzdGFydF90b2tlbnM6IGxpc3Q9WyIlIiwgInsiXQogICAgZW5kX3Rva2VuczogbGlzdD1bIiUiLCAifSJdCiAgICBzdGFjayA9IGRlcXVlKFtdLCAyKQogICAgc3RhcnRfdG9rZW5zX2NoYXJzOiBzdHIgPSAiIi5qb2luKHN0YXJ0X3Rva2VucykKICAgIGVuZF90b2tlbnNfY2hhcnM6IHN0ciA9ICIiLmpvaW4oZW5kX3Rva2VucykKICAgIHN0YXJ0X3Rva2VuX2ZvdW5kOiBib29sID0gRmFsc2UKICAgIGVuZF90b2tlbl9mb3VuZDogYm9vbCA9IEZhbHNlCiAgICB2YXJfbmFtZTogc3RyID0gIiIKICAgIGFsbG93ZWRfY2hhcnM6IHN0ciA9ICJhYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ejEyMzQ1Njc4OTBfIgogICAgbGFzdF9jaGFyOiBzdHIgPSAiIgogICAgc3RhcnRfdG9rZW46IHN0ciA9ICIiCiAgICBlbmRfdG9rZW46IHN0ciA9ICIiCiAgICBuZXdfc3RyaW5nOiBzdHIgPSAiIgogICAgZm9yIGMgaW4gc3RyaW5nOgogICAgICAgIGlmIChub3Qgc3RhcnRfdG9rZW5fZm91bmQgYW5kIGMgaW4gc3RhcnRfdG9rZW5zX2NoYXJzKSBvciAoc3RhcnRfdG9rZW5fZm91bmQgYW5kIGMgaW4gZW5kX3Rva2Vuc19jaGFycykgYW5kIGxhc3RfY2hhciAhPSAiXFwiOgogICAgICAgICAgICBzdGFjay5hcHBlbmQoYykKICAgICAgICBlbHNlOgogICAgICAgICAgICBzdGFjay5hcHBlbmQoIiAiKQogICAgICAgIHRva2VuID0gIiIuam9pbihzdGFjaykKICAgICAgICBpZiBzdGFydF90b2tlbl9mb3VuZDoKICAgICAgICAgICAgZm9yIHQgaW4gKGMsIHRva2VuKToKICAgICAgICAgICAgICAgIGlmIHQgaW4gZW5kX3Rva2VuczoKICAgICAgICAgICAgICAgICAgICBpZiBzdGFydF90b2tlbnMuaW5kZXgoc3RhcnRfdG9rZW4pID09IGVuZF90b2tlbnMuaW5kZXgodCk6CiAgICAgICAgICAgICAgICAgICAgICAgIGVuZF90b2tlbiA9IHQKICAgICAgICAgICAgICAgICAgICAgICAgdmFsdWUgPSBkYXRhLmdldCh2YXJfbmFtZSwgZiJ7c3RhcnRfdG9rZW59e3Zhcl9uYW1lfXtlbmRfdG9rZW59IikKICAgICAgICAgICAgICAgICAgICAgICAgaWYgaXNpbnN0YW5jZSh2YWx1ZSwgKGxpc3QsIGRpY3QpKToKICAgICAgICAgICAgICAgICAgICAgICAgICAgIHZhbHVlID0ganNvbi5kdW1wcyh2YWx1ZSkKICAgICAgICAgICAgICAgICAgICAgICAgZWxzZToKICAgICAgICAgICAgICAgICAgICAgICAgICAgIHZhbHVlID0gc3RyKHZhbHVlKQogICAgICAgICAgICAgICAgICAgICAgICBuZXdfc3RyaW5nICs9IGh0bWwuZXNjYXBlKHZhbHVlKQogICAgICAgICAgICAgICAgICAgICAgICBzdGFydF90b2tlbl9mb3VuZCA9IEZhbHNlCiAgICAgICAgICAgICAgICAgICAgICAgIGVuZF90b2tlbl9mb3VuZCA9IFRydWUKICAgICAgICAgICAgICAgICAgICAgICAgdmFyX25hbWUgPSAiIgogICAgICAgICAgICAgICAgICAgICAgICBicmVhawogICAgICAgICAgICAgICAgICAgIGVsc2U6CiAgICAgICAgICAgICAgICAgICAgICAgIHByaW50KCJlbmQgdG9rZW4gZG9lcyBub3QgbWF0Y2ggc3RhcnQgdG9rZW4hIikKICAgICAgICAgICAgICAgICAgICAgICAgc3RhcnRfdG9rZW5fZm91bmQgPSBGYWxzZQogICAgICAgICAgICAgICAgICAgICAgICBuZXdfc3RyaW5nICs9IHN0YXJ0X3Rva2VuICsgdmFyX25hbWUgKyBjCiAgICAgICAgICAgICAgICAgICAgICAgIHZhcl9uYW1lID0gIiIKICAgICAgICAgICAgZWxzZToKICAgICAgICAgICAgICAgIGlmIGMgaW4gZW5kX3Rva2Vuc19jaGFyczoKICAgICAgICAgICAgICAgICAgICBsYXN0X2NoYXIgPSBjCiAgICAgICAgICAgICAgICAgICAgY29udGludWUKICAgICAgICAgICAgICAgIGlmIGMgaW4gYWxsb3dlZF9jaGFyczoKICAgICAgICAgICAgICAgICAgICB2YXJfbmFtZSArPSBjCiAgICAgICAgICAgICAgICAgICAgY29udGludWUKICAgICAgICAgICAgICAgIGVsc2U6CiAgICAgICAgICAgICAgICAgICAgc3RhcnRfdG9rZW5fZm91bmQgPSBGYWxzZQogICAgICAgICAgICAgICAgICAgIHByaW50KGYie2N9IGlzIG5vdCBhbGxvd2VkIGluc2lkZSBwbGFjZWhvbGRlciIpCiAgICAgICAgICAgICAgICAgICAgbmV3X3N0cmluZyArPSBzdGFydF90b2tlbiArIHZhcl9uYW1lICsgYwogICAgICAgICAgICAgICAgICAgIHZhcl9uYW1lID0gIiIKICAgICAgICAgICAgICAgIGNvbnRpbnVlCiAgICAgICAgICAgIGlmIGVuZF90b2tlbl9mb3VuZDoKICAgICAgICAgICAgICAgIGxhc3RfY2hhciA9IGMKICAgICAgICAgICAgICAgIGVuZF90b2tlbl9mb3VuZCA9IEZhbHNlCiAgICAgICAgICAgICAgICBjb250aW51ZQogICAgICAgIGlmIG5vdCBzdGFydF90b2tlbl9mb3VuZDoKICAgICAgICAgICAgZm9yIHQgaW4gKGMsIHRva2VuKToKICAgICAgICAgICAgICAgIGlmIHQgaW4gc3RhcnRfdG9rZW5zOgogICAgICAgICAgICAgICAgICAgIHN0YXJ0X3Rva2VuX2ZvdW5kID0gVHJ1ZQogICAgICAgICAgICAgICAgICAgIHN0YXJ0X3Rva2VuID0gdAogICAgICAgICAgICAgICAgICAgIGJyZWFrCiAgICAgICAgaWYgYyBub3QgaW4gc3RhcnRfdG9rZW5zX2NoYXJzIGFuZCBub3QgKHN0YXJ0X3Rva2VuX2ZvdW5kIGFuZCBjIGluIGVuZF90b2tlbnNfY2hhcnMpIG9yIGxhc3RfY2hhciA9PSAiXFwiOgogICAgICAgICAgICBuZXdfc3RyaW5nICs9IGMKICAgICAgICBsYXN0X2NoYXIgPSBjCiAgICByZXR1cm4gbmV3X3N0cmluZwoKCmRlZiBmb3JtYXRfd2ViKHN0cmluZzogc3RyLCBkYXRhKToKICAgIHN0YXJ0X3Rva2VuX2ZvdW5kOiBib29sID0gRmFsc2UKICAgIHZhcl9uYW1lOiBzdHIgPSAiIgogICAgYWxsb3dlZF9jaGFyczogc3RyID0gImFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6MTIzNDU2Nzg5MF8iCiAgICBsYXN0X2NoYXI6IHN0ciA9ICIiCiAgICBzdGFydF90b2tlbjogc3RyID0gIiUiCiAgICBlbmRfdG9rZW46IHN0ciA9ICIlIgogICAgbmV3X3N0cmluZzogc3RyID0gIiIKICAgIGZvciBjIGluIHN0cmluZzoKICAgICAgICBpZiBzdGFydF90b2tlbl9mb3VuZDoKICAgICAgICAgICAgaWYgYyA9PSBlbmRfdG9rZW46CiAgICAgICAgICAgICAgICB2YWx1ZSA9IGRhdGEuZ2V0KHZhcl9uYW1lLCBmIntzdGFydF90b2tlbn17dmFyX25hbWV9e2VuZF90b2tlbn0iKQogICAgICAgICAgICAgICAgaWYgaXNpbnN0YW5jZSh2YWx1ZSwgKGxpc3QsIGRpY3QpKToKICAgICAgICAgICAgICAgICAgICAgdmFsdWUgPSBqc29uLmR1bXBzKHZhbHVlKQogICAgICAgICAgICAgICAgZWxzZToKICAgICAgICAgICAgICAgICAgICB2YWx1ZSA9IHN0cih2YWx1ZSkKICAgICAgICAgICAgICAgIG5ld19zdHJpbmcgKz0gaHRtbC5lc2NhcGUodmFsdWUpCiAgICAgICAgICAgICAgICBzdGFydF90b2tlbl9mb3VuZCA9IEZhbHNlCiAgICAgICAgICAgICAgICB2YXJfbmFtZSA9ICIiCiAgICAgICAgICAgICAgICBsYXN0X2NoYXIgPSBjCiAgICAgICAgICAgICAgICBjb250aW51ZQogICAgICAgICAgICBlbHNlOgogICAgICAgICAgICAgICAgaWYgYyBpbiBhbGxvd2VkX2NoYXJzOgogICAgICAgICAgICAgICAgICAgIHZhcl9uYW1lICs9IGMKICAgICAgICAgICAgICAgICAgICBjb250aW51ZQogICAgICAgICAgICAgICAgZWxzZToKICAgICAgICAgICAgICAgICAgICBzdGFydF90b2tlbl9mb3VuZCA9IEZhbHNlCiAgICAgICAgICAgICAgICAgICAgcHJpbnQoZiJ7Y30gaXMgbm90IGFsbG93ZWQgaW5zaWRlIHBsYWNlaG9sZGVyIikKICAgICAgICAgICAgICAgICAgICBuZXdfc3RyaW5nICs9IHN0YXJ0X3Rva2VuICsgdmFyX25hbWUgKyBjCiAgICAgICAgICAgICAgICAgICAgdmFyX25hbWUgPSAiIgogICAgICAgICAgICAgICAgY29udGludWUKICAgICAgICBlbHNlOgogICAgICAgICAgICBpZiAgbGFzdF9jaGFyICE9ICJcXCIgYW5kIGMgPT0gc3RhcnRfdG9rZW46CiAgICAgICAgICAgICAgICBzdGFydF90b2tlbl9mb3VuZCA9IFRydWUKICAgICAgICAgICAgICAgIGxhc3RfY2hhciA9IGMKICAgICAgICAgICAgICAgIGNvbnRpbnVlCiAgICAgICAgaWYgYyAhPSBzdGFydF90b2tlbiBhbmQgbm90IChzdGFydF90b2tlbl9mb3VuZCBhbmQgYyA9PSBlbmRfdG9rZW4pIG9yIGxhc3RfY2hhciA9PSAiXFwiOgogICAgICAgICAgICBuZXdfc3RyaW5nICs9IGMKICAgICAgICBsYXN0X2NoYXIgPSBjCiAgICByZXR1cm4gbmV3X3N0cmluZwoKCkBiaW5kKHNlbGYsICJtZXNzYWdlIikKZGVmIG1lc3NhZ2UoZXZ0KToKICAgICIiIkhhbmRsZSBhIG1lc3NhZ2Ugc2VudCBieSB0aGUgbWFpbiBzY3JpcHQuCiAgICBldnQuZGF0YSBpcyB0aGUgbWVzc2FnZSBib2R5LgogICAgIiIiCiAgICBjb25zb2xlLmRlYnVnKCJuZXcgbWVzc2FnZSByZWNlaXZlZCIpCiAgICAjc2VsZi5zZW5kKGZvcm1hdCgqKmV2dC5kYXRhKSkKICAgIHNlbGYuc2VuZChmb3JtYXRfd2ViKGV2dC5kYXRhWzBdLCBldnQuZGF0YVsxXSkp\n",
        "ctime": 1726502204.3286872,
        "mtime": 1719139986.8821394
    }
});

formatter0.py

#!/usr/bin/env python3

# Standard library imports.
from collections import deque
#from browser.webworker import current_worker, Message
from sys import argv
from os import environ
import html
# In web workers, "window" is replaced by "self".
from browser import bind, self, console
import json

# Related third party imports.

# Local application/library specific imports.
#from stermi import formatter

console.debug("started format web worker")


def format(string: str, data):
    start_tokens: list=["%", "{"]
    end_tokens: list=["%", "}"]
    stack = deque([], 2)
    start_tokens_chars: str = "".join(start_tokens)
    end_tokens_chars: str = "".join(end_tokens)
    start_token_found: bool = False
    end_token_found: bool = False
    var_name: str = ""
    allowed_chars: str = "abcdefghijklmnopqrstuvwxyz1234567890_"
    last_char: str = ""
    start_token: str = ""
    end_token: str = ""
    new_string: str = ""
    for c in string:
        if (not start_token_found and c in start_tokens_chars) or (start_token_found and c in end_tokens_chars) and last_char != "\\":
            stack.append(c)
        else:
            stack.append(" ")
        token = "".join(stack)
        if start_token_found:
            for t in (c, token):
                if t in end_tokens:
                    if start_tokens.index(start_token) == end_tokens.index(t):
                        end_token = t
                        value = data.get(var_name, f"{start_token}{var_name}{end_token}")
                        if isinstance(value, (list, dict)):
                            value = json.dumps(value)
                        else:
                            value = str(value)
                        new_string += html.escape(value)
                        start_token_found = False
                        end_token_found = True
                        var_name = ""
                        break
                    else:
                        print("end token does not match start token!")
                        start_token_found = False
                        new_string += start_token + var_name + c
                        var_name = ""
            else:
                if c in end_tokens_chars:
                    last_char = c
                    continue
                if c in allowed_chars:
                    var_name += c
                    continue
                else:
                    start_token_found = False
                    print(f"{c} is not allowed inside placeholder")
                    new_string += start_token + var_name + c
                    var_name = ""
                continue
            if end_token_found:
                last_char = c
                end_token_found = False
                continue
        if not start_token_found:
            for t in (c, token):
                if t in start_tokens:
                    start_token_found = True
                    start_token = t
                    break
        if c not in start_tokens_chars and not (start_token_found and c in end_tokens_chars) or last_char == "\\":
            new_string += c
        last_char = c
    return new_string


def format_web(string: str, data):
    start_token_found: bool = False
    var_name: str = ""
    allowed_chars: str = "abcdefghijklmnopqrstuvwxyz1234567890_"
    last_char: str = ""
    start_token: str = "%"
    end_token: str = "%"
    new_string: str = ""
    for c in string:
        if start_token_found:
            if c == end_token:
                value = data.get(var_name, f"{start_token}{var_name}{end_token}")
                if isinstance(value, (list, dict)):
                     value = json.dumps(value)
                else:
                    value = str(value)
                new_string += html.escape(value)
                start_token_found = False
                var_name = ""
                last_char = c
                continue
            else:
                if c in allowed_chars:
                    var_name += c
                    continue
                else:
                    start_token_found = False
                    print(f"{c} is not allowed inside placeholder")
                    new_string += start_token + var_name + c
                    var_name = ""
                continue
        else:
            if  last_char != "\\" and c == start_token:
                start_token_found = True
                last_char = c
                continue
        if c != start_token and not (start_token_found and c == end_token) or last_char == "\\":
            new_string += c
        last_char = c
    return new_string


@bind(self, "message")
def message(evt):
    """Handle a message sent by the main script.
    evt.data is the message body.
    """
    console.debug("new message received")
    #self.send(format(**evt.data))
    self.send(format_web(evt.data[0], evt.data[1]))