Repository: HapticX/happyx (language: python)

Issue:
set buildHtml root attrs to "display: contents;"
**Is your feature request related to a problem? Please describe.** 🤔
One kind of undocumented syntax with buildHtml is that the content sit in a div. This makes the built html content different from the literal content of the DSLs body in a couple of significant ways. First, it imposes mandatory line breaks pretty much all the time. Second, it disrupts parent-child relationships between tags.

**Describe the solution you'd like** 💡
"display: contents;"  is the CSS setting made to get around this and is designed with computer-generated html in mind where divs might be written without any semantic intent. While it may be nice to have the content enclosed in a div automatically in some circumstances, it's much harder to opt-out of being in a div once it is baked in to `buildHtml `and `buildHtmlProcedure.` It seems like a div with "display: contents" would change the existing code minimally while making it easier to reason about and use [build/thunk]Html data.


Discussion / hints:

Fix patch:
diff --git a/.github/workflows/examples.yml b/.github/workflows/examples.yml
index 38ffde7bc..c972541a5 100644
--- a/.github/workflows/examples.yml
+++ b/.github/workflows/examples.yml
@@ -39,28 +39,28 @@ jobs:
         os:
           - ubuntu-latest
         nim_version:
-          - '2.0.4'
+          - '2.0.6'
     env:
       TIMEOUT_EXIT_STATUS: 124
     steps:
       - uses: actions/checkout@v4
-      - uses: actions/cache@v4
-        with:
-          path:
-            ~/.nimble
-            ~/.choosenim
-          key: ${{ runner.os }}-nimble-${{ hashFiles('*.nimble') }}
-      - uses: jiro4989/setup-nim-action@v1
-        with:
-          nim-version: ${{ matrix.nim_version }}
-      
-      - name: Install Dependencies
+
+      - name: Install Nim 👑
         run: |
+          sudo apt install xz-utils
+          curl https://nim-lang.org/download/nim-${{ matrix.nim_version }}-linux_x64.tar.xz --output nim.tar.xz
+          unxz nim.tar.xz
+          tar -xf nim.tar
+          sudo mkdir -p /home/.nimble
+          sudo mv -if nim-${{ matrix.nim_version }}/* /home/.nimble/
+          export PATH=/home/.nimble/bin:$PATH
+          nim -v
           nimble refresh
-          nimble install -y -d
+          nimble install -y
       
       - name: Hello World 👋
         run: |
+          export PATH=/home/.nimble/bin:$PATH
           cd examples/hello_world/src
           nim c -d:httpx main
           cd ../../../
@@ -68,6 +68,7 @@ jobs:
       
       - name: TODO App ✅
         run: |
+          export PATH=/home/.nimble/bin:$PATH
           cd examples/todo/src
           nim js --hints:off --warnings:off main
           cd ../../../
@@ -75,6 +76,7 @@ jobs:
       
       - name: NimLang 👑
         run: |
+          export PATH=/home/.nimble/bin:$PATH
           cd examples/nimlang/src
           nimble install highlightjs
           nim js --hints:off --warnings:off main
@@ -83,6 +85,7 @@ jobs:
       
       - name: Website 🌐
         run: |
+          export PATH=/home/.nimble/bin:$PATH
           cd examples/website/src
           nim js --hints:off --warnings:off main
           cd ../../../
diff --git a/.github/workflows/gh-pages.yml b/.github/workflows/gh-pages.yml
index 41a24e73e..3ba03fdfb 100644
--- a/.github/workflows/gh-pages.yml
+++ b/.github/workflows/gh-pages.yml
@@ -20,17 +20,10 @@ permissions:
   contents: write
 
 jobs:
-  before:
-    runs-on: windows-latest
-    if: "! contains(github.event.head_commit.message, '[skip ci]')"
-    steps:
-      - run: echo "not contains '[skip ci]'"
-
   docs:
     runs-on: windows-latest
-    needs: before
     env:
-      nim_version: '2.0.4'
+      nim_version: '2.0.6'
       node_version: '20'
     steps:
       - uses: actions/checkout@v4
@@ -44,28 +37,28 @@ jobs:
         run: |
           npm install uglify-js -g
 
-      - name: Cache nimble
-        id: cache-nimble
-        uses: actions/cache@v1
-        with:
-          path: ~/.nimble
-          key: ${{ runner.os }}-nimble-${{ hashFiles('*.nimble') }}
-      
-      - uses: jiro4989/setup-nim-action@v1
-        with:
-          nim-version: ${{ env.nim_version }}
-
-      - name: Install Dependencies 🔃
+      - name: Install Nim 👑
         run: |
+          sudo apt install xz-utils
+          curl https://nim-lang.org/download/nim-${{ env.nim_version }}-linux_x64.tar.xz --output nim.tar.xz
+          unxz nim.tar.xz
+          tar -xf nim.tar
+          sudo mkdir -p /home/.nimble
+          sudo mv -if nim-${{ env.nim_version }}/* /home/.nimble/
+          export PATH=/home/.nimble/bin:$PATH
+          nim -v
           nimble refresh
           nimble install -y
 
       - name: Generate API documents 📃
-        run: nim doc --index:on -d:docgen --project --out:docs --hints:off src/happyx.nim
+        run: |
+          export PATH=/home/.nimble/bin:$PATH
+          nim doc --index:on -d:docgen --project --out:docs --hints:off src/happyx.nim
       
       - name: Build HappyX website 🌐
         timeout-minutes: 2
         run: |
+          export PATH=/home/.nimble/bin:$PATH
           cd ./examples/website/src/
           nim js -d:production -d:release --opt:size --hints:off --warnings:off --out:main_full.js main
           uglifyjs main_full.js -c -m --mangle-props regex=/N[ST]I\w+/ -o main.js
diff --git a/.github/workflows/language_bindings.yml b/.github/workflows/language_bindings.yml
index ac03a45b0..bee8c8e74 100644
--- a/.github/workflows/language_bindings.yml
+++ b/.github/workflows/language_bindings.yml
@@ -19,40 +19,8 @@ on:
       - '**/constants.nim'
 
 jobs:
-  dependencies:
-    name: Install Dependencies 🔨
-    runs-on: ${{ matrix.os }}
-    strategy:
-      matrix:
-        os:
-          - ubuntu-latest
-        nim_version:
-          - '2.0.4'
-    env:
-      TIMEOUT_EXIT_STATUS: 124
-    steps:
-      - uses: actions/checkout@v4
-      - uses: jiro4989/setup-nim-action@v1
-        with:
-          nim-version: ${{ matrix.nim_version }}
-      - name: Install dependencies 🔃
-        run: |
-          nimble refresh
-          nimble install -y -d
-          nimble install -y jnim
-          nimble install -y checksums
-          nimble list --installed
-          nim -v
-      - uses: actions/cache@v4
-        with:
-          path: |
-            ~/.nimble
-            ~/.choosenim
-          key: ${{ runner.os }}-nimble-${{ hashFiles('*.nimble') }}
-  
   python:
     name: Python bindings 🐍
-    needs: dependencies
     runs-on: ${{ matrix.os }}
     strategy:
       matrix:
@@ -60,16 +28,24 @@ jobs:
           - ubuntu-latest
         python_version:
           - '3.x'
+        nim_version:
+          - '2.0.6'
     env:
       TIMEOUT_EXIT_STATUS: 124
     steps:
       - uses: actions/checkout@v4
-      - uses: actions/cache@v4
-        with:
-          path: |
-            ~/.nimble
-            ~/.choosenim
-          key: ${{ runner.os }}-nimble-${{ hashFiles('*.nimble') }}
+
+      - name: Install Nim 👑
+        run: |
+          sudo apt install xz-utils
+          curl https://nim-lang.org/download/nim-${{ matrix.nim_version }}-linux_x64.tar.xz --output nim.tar.xz
+          unxz nim.tar.xz
+          tar -xf nim.tar
+          sudo mkdir -p /home/.nimble
+          sudo mv -if nim-${{ matrix.nim_version }}/* /home/.nimble/
+          export PATH=/home/.nimble/bin:$PATH
+          nim -v
+      
       - uses: actions/setup-python@v4
         with:
           python-version: ${{ matrix.python_version }}
@@ -79,12 +55,17 @@ jobs:
           pip install -U twine build packaging
       - name: Python Bindings 🐍
         run: |
+          export PATH=/home/.nimble/bin:$PATH
+          nimble refresh
+          nimble install -y -d
+          nimble install -y checksums
+          nimble list --installed
           cd src
           echo "Windows module"
           echo "Win Module compiled with"
           echo "nim c --app:lib --out:../bindings/python/happyx/happyx.pyd -d:useRealtimeGC --mm:arc --tlsEmulation:off --passL:-static --t:-flto --l:-flto --opt:speed --threads:off -d:release -d:httpx -d:export2py happyx"
           echo "Linux module"
-          /home/runner/.nimble/bin/nim c --app:lib --out:../bindings/python/happyx/happyx.so -d:useRealtimeGC --mm:arc -t:-flto -l:-flto --opt:speed --threads:off -d:release -d:happyxDebug -x:off -a:off -d:beast -d:export2py happyx
+          nim c --app:lib --out:../bindings/python/happyxpy/happyx.so -d:useRealtimeGC --mm:arc -t:-flto -l:-flto --opt:speed --threads:off -d:release -d:happyxDebug -x:off -a:off -d:beast -d:export2py happyx
         shell: bash
       - name: Build Python Package 🏗
         run: |
@@ -92,24 +73,23 @@ jobs:
           python -m build
       # TODO:
       #   account recovery
-      # - name: Publish Python package 📢
-      #   uses: pypa/gh-action-pypi-publish@8cdc2ab67c943c5edf5fd9ae1995546b4b550602
-      #   with:
-      #     user: __token__
-      #     password: ${{ secrets.PYPI_API_TOKEN }}
-      #     packages_dir: bindings/python/dist/
-      #     verbose: true
+      - name: Publish Python package 📢
+        uses: pypa/gh-action-pypi-publish@8cdc2ab67c943c5edf5fd9ae1995546b4b550602
+        with:
+          user: __token__
+          password: ${{ secrets.PYPI_API_TOKEN }}
+          packages_dir: bindings/python/dist/
+          verbose: true
   
   jvm:
     name: JVM Bindings ☕
-    needs: dependencies
     runs-on: ${{ matrix.os }}
     strategy:
       matrix:
         os:
           - ubuntu-latest
         nim_version:
-          - '2.0.4'
+          - '2.0.6'
         java_version:
           - '16'
         gradle_version:
@@ -118,11 +98,21 @@ jobs:
       TIMEOUT_EXIT_STATUS: 124
     steps:
       - uses: actions/checkout@v4
-      - uses: jiro4989/setup-nim-action@v1
-        with:
-          nim-version: ${{ matrix.nim_version }}
+
+      - name: Install Nim 👑
+        run: |
+          sudo apt install xz-utils
+          curl https://nim-lang.org/download/nim-${{ matrix.nim_version }}-linux_x64.tar.xz --output nim.tar.xz
+          unxz nim.tar.xz
+          tar -xf nim.tar
+          sudo mkdir -p /home/.nimble
+          sudo mv -if nim-${{ matrix.nim_version }}/* /home/.nimble/
+          export PATH=/home/.nimble/bin:$PATH
+          nim -v
+
       - name: Install dependencies 🔃
         run: |
+          export PATH=/home/.nimble/bin:$PATH
           nimble refresh
           nimble install -y -d
           nimble install -y jnim
@@ -141,9 +131,10 @@ jobs:
           cache: 'gradle'
       - name: JVM Bindings ☕
         run: |
+          export PATH=/home/.nimble/bin:$PATH
           cd src
           echo "Unix module"
-          /home/runner/.nimble/bin/nim c -d:noSignalHandler -d:export2jvm -d:release -d:httpx --app:lib --threads:on --opt:speed --mm:arc --out:../bindings/java/src/main/resources/happyx.so happyx.nim
+          nim c -d:noSignalHandler -d:export2jvm -d:release -d:httpx --app:lib --threads:on --opt:speed --mm:arc --out:../bindings/java/src/main/resources/happyx.so happyx.nim
           cd ..
       - name: Build Java Project ☕
         run: |
diff --git a/README.md b/README.md
index 06d6602fd..d4a64071a 100755
--- a/README.md
+++ b/README.md
@@ -53,7 +53,7 @@ HappyX is macro-oriented web framework so you mustn't write a lot of code ✨
 - Automatic translate with `-d:hpxTranslate` or `-d:translate` flags.
 - CLI tool for `creating`, `serving` and `building` your projects.
 - Hot code reloading (only for `SPA` projects as of now).
-- [Python bindings](https://pypi.org/project/happyx/)
+- [Python bindings](https://pypi.org/project/happyxpy/)
 - [NodeJS bindings](https://www.npmjs.com/package/happyx)
 
 ## Minimal Example 👩‍💻
@@ -101,7 +101,7 @@ nimble install https://github.com/HapticX/happyx
 
 ### PyPI
 ```shell
-pip install happyx
+pip install happyxpy
 ```
 
 ## Usage ▶
diff --git a/bindings/python/MANIFEST.in b/bindings/python/MANIFEST.in
index bee3b735d..e373ebe0f 100644
--- a/bindings/python/MANIFEST.in
+++ b/bindings/python/MANIFEST.in
@@ -1,3 +1,3 @@
 # MANIFEST.in
-include happyx/*.pyd
-include happyx/*.so
+include happyxpy/*.pyd
+include happyxpy/*.so
diff --git a/bindings/python/README.md b/bindings/python/README.md
index d78db77aa..6f539749a 100644
--- a/bindings/python/README.md
+++ b/bindings/python/README.md
@@ -15,7 +15,7 @@
 
 You can install HappyX via `pypi`:
 ```bash
-pip install happyx
+pip install happyxpy
 ```
 
 ## Usage 🔌
@@ -23,7 +23,7 @@ pip install happyx
 ### Hello World 👋
 
 ```py
-from happyx import Server
+from happyxpy import Server
 
 
 app = Server('127.0.0.1', 5000)  # host and port are optional params
@@ -41,7 +41,7 @@ app.start()
 ### JSON/HTML/File Responses 🛠
 
 ```py
-from happyx import Server, JsonResponse, HtmlResponse, FileResponse
+from happyxpy import Server, JsonResponse, HtmlResponse, FileResponse
 
 
 app = Server()
diff --git a/bindings/python/happyx/happyx.pyd b/bindings/python/happyx/happyx.pyd
deleted file mode 100644
index 352739dd3..000000000
Binary files a/bindings/python/happyx/happyx.pyd and /dev/null differ
diff --git a/bindings/python/happyx/__init__.py b/bindings/python/happyxpy/__init__.py
similarity index 100%
rename from bindings/python/happyx/__init__.py
rename to bindings/python/happyxpy/__init__.py
diff --git a/bindings/python/happyx/constants.py b/bindings/python/happyxpy/constants.py
similarity index 100%
rename from bindings/python/happyx/constants.py
rename to bindings/python/happyxpy/constants.py
diff --git a/bindings/python/happyxpy/happyx.pyd b/bindings/python/happyxpy/happyx.pyd
new file mode 100644
index 000000000..316b404f6
Binary files /dev/null and b/bindings/python/happyxpy/happyx.pyd differ
diff --git a/bindings/python/happyx/server.py b/bindings/python/happyxpy/server.py
similarity index 99%
rename from bindings/python/happyx/server.py
rename to bindings/python/happyxpy/server.py
index cd0ffd4e6..4b115928e 100644
--- a/bindings/python/happyx/server.py
+++ b/bindings/python/happyxpy/server.py
@@ -4,7 +4,7 @@
 
 from .constants import SWAGGER_HTML_SOURCE, REDOC_HTML_SOURCE
 
-import happyx.happyx as happyx
+import happyxpy.happyx as happyx
 
 
 Callback = Callable[[Any], Any]
diff --git a/bindings/python/setup.py b/bindings/python/setup.py
index 16ffcd618..6ecf94deb 100644
--- a/bindings/python/setup.py
+++ b/bindings/python/setup.py
@@ -4,7 +4,7 @@
 from os import path, makedirs
 from setuptools import setup, find_packages, version
 
-import happyx.happyx as happyx
+import happyxpy.happyx as happyx
 
 
 # Load readme
@@ -13,7 +13,7 @@
 
 
 setup(
-    name='happyx',
+    name='happyxpy',
     description='HappyX web framework bindings for Python 🐍',
     long_description=long_description,
     long_description_content_type='text/markdown',
@@ -40,4 +40,4 @@
         'Programming Language :: Python :: 3.11',
         'Programming Language :: Python :: 3.12',
     ]
-)
\ No newline at end of file
+)
diff --git a/build_py.cmd b/build_py.cmd
index 422006f79..21c217481 100644
--- a/build_py.cmd
+++ b/build_py.cmd
@@ -3,7 +3,7 @@ cd src
 
 nim c ^
   --app:lib ^
-  --out:../bindings/python/happyx/happyx.pyd ^
+  --out:../bindings/python/happyxpy/happyx.pyd ^
   --tlsEmulation:off ^
   --mm:arc ^
   --passL:-static ^
diff --git a/build_py_debug.cmd b/build_py_debug.cmd
index 84f90fbd9..0e4653b9d 100644
--- a/build_py_debug.cmd
+++ b/build_py_debug.cmd
@@ -1,4 +1,4 @@
 echo "Build Python Win Bindings"
 cd src
-nim c --app:lib --out:../bindings/python/happyx/happyx.pyd -d:useRealtimeGC --mm:arc --tlsEmulation:off --passL:-static --threads:off -d:debug -d:httpx -d:export2py happyx
+nim c --app:lib --out:../bindings/python/happyxpy/happyx.pyd -d:useRealtimeGC --mm:arc --tlsEmulation:off --passL:-static --threads:off -d:debug -d:httpx -d:export2py happyx
 cd ../
diff --git a/examples/website/src/components/guide_page.nim b/examples/website/src/components/guide_page.nim
index d37f5a946..e5f359e36 100644
--- a/examples/website/src/components/guide_page.nim
+++ b/examples/website/src/components/guide_page.nim
@@ -53,6 +53,8 @@ component GuidePage:
           SsrDocs
         of "mounting":
           Mounting
+        of "hpx_for_karax":
+          KaraxUsers
 
       tDiv(class = "hidden xl:flex justify-between items-center w-full pb-8"):
         if guidePages[currentGuidePage]["prev"].getStr != "":
diff --git a/examples/website/src/components/sidebar.nim b/examples/website/src/components/sidebar.nim
index 7a6daa9c9..f7e33025b 100644
--- a/examples/website/src/components/sidebar.nim
+++ b/examples/website/src/components/sidebar.nim
@@ -101,6 +101,11 @@ var
     "ssr_docs": {
       "title": "Swagger and Redoc in HappyX 📕",
       "prev": "mongo_db",
+      "next": "hpx_for_karax"
+    },
+    "hpx_for_karax": {
+      "title": "Karax users 👑",
+      "prev": "ssr_docs",
       "next": ""
     },
   }
@@ -214,6 +219,9 @@ proc SideBar*(isMobile: bool = false): TagRef =
                 {translate"MongoDB 🍃"}
               SideBarItem("ssr_docs", isMobile):
                 {translate"Swagger and Redoc in HappyX 📕"}
+            SideBarFolder("hpx_for_karax", "HappyX for ...", isMobile):
+              SideBarItem("hpx_for_karax", isMobile):
+                {translate"Karax users 👑"}
         tDiv(class = "flex"):
           Button(
             action = proc() =
diff --git a/examples/website/src/docs/docs.nim b/examples/website/src/docs/docs.nim
index c6f69b487..80929219a 100644
--- a/examples/website/src/docs/docs.nim
+++ b/examples/website/src/docs/docs.nim
@@ -7,9 +7,13 @@ import
   ],
   ./db/[
     mongo, sqlite, postgres, db_access
+  ],
+  ./for_users/[
+    karax
   ]
 
 export
   introduction, getting_started, happyx_app, path_params, spa_basics, ssr_basics,
   tailwind_and_other, decorators, reactivity, mongo, db_access, sqlite, postgres,
-  components, ssr_docs, func_components, spa_rendering, mounting
+  components, ssr_docs, func_components, spa_rendering, mounting,
+  karax
diff --git a/examples/website/src/docs/for_users/karax.nim b/examples/website/src/docs/for_users/karax.nim
new file mode 100644
index 000000000..f304a3607
--- /dev/null
+++ b/examples/website/src/docs/for_users/karax.nim
@@ -0,0 +1,47 @@
+# Import HappyX
+import
+  ../../../../../src/happyx,
+  ../../ui/[colors, code, play_states, translations],
+  ../../components/[
+    code_block_guide, code_block, code_block_slider, tip
+  ]
+
+
+proc KaraxUsers*(): TagRef =
+  buildHtml:
+    tDiv(class = "flex flex-col px-8 py-2 xl:h-fit gap-4"):
+      tH1: {translate"HappyX for Karax users 👑"}
+
+      tP: {translate"This article discusses the differences between Karax and HappyX, as well as their pros and cons."}
+
+      tH2: "Hello, world!"
+
+      tP: {translate"I propose to consider the first example from the Karax README:"}
+
+      CodeBlock("nim", karaxHelloWorld, "nim_vs_karax_karax_ex1")
+
+      tP: {translate"Here's how you can rewrite it using HappyX:"}
+
+      CodeBlock("nim", happyxVsKaraxHelloWorld1, "nim_vs_karax_happyx_ex1")
+
+      tP:
+        {translate"However, if you look again, you can see that the Karax example uses a function that returns VDOM."}
+        " "
+        {translate"In HappyX, you can also use functions to generate VDOM:"}
+
+      CodeBlock("nim", happyxVsKaraxHelloWorld2, "nim_vs_karax_happyx_ex2")
+
+      tH2: {translate"The event model"}
+
+      tP:
+        {translate"Now let's consider the event model provided by HappyX and Karax."}
+
+      CodeBlock("nim", karaxEventModel, "nim_vs_karax_karax_ex2")
+      CodeBlock("nim", happyxVsKaraxEventModel, "nim_vs_karax_happyx_ex3")
+
+      tP:
+        {translate"Unlike Karax, HappyX does not change the event model and allows you to embed as many events as you like on a single element."}
+        " "
+        {translate"You are also free to change the variable name responsible for the event:"}
+      CodeBlock("nim", happyxVsKaraxEventModel1, "nim_vs_karax_happyx_ex4")
+      
diff --git a/examples/website/src/ui/code.nim b/examples/website/src/ui/code.nim
index 8dc914af6..1b9d995fa 100644
--- a/examples/website/src/ui/code.nim
+++ b/examples/website/src/ui/code.nim
@@ -1,10 +1,10 @@
 import
   code/[
     python, java, kotlin, javascript, typescript,
-    nim_spa, nim_ssr
+    nim_spa, nim_ssr, karax
   ]
 
 
 export
   python, java, kotlin, javascript, typescript,
-  nim_spa, nim_ssr
+  nim_spa, nim_ssr, karax
diff --git a/examples/website/src/ui/code/karax.nim b/examples/website/src/ui/code/karax.nim
new file mode 100644
index 000000000..a7ba031fe
--- /dev/null
+++ b/examples/website/src/ui/code/karax.nim
@@ -0,0 +1,68 @@
+const
+  karaxHelloWorld* = """include karax / prelude
+
+proc createDom(): VNode =
+  result = buildHtml(tdiv):
+    text "Hello World!"
+
+setRenderer createDom
+"""
+  happyxVsKaraxHelloWorld1* = """import happyx
+
+appRoutes "app":
+  "/":
+    "Hello, world!"
+"""
+  happyxVsKaraxHelloWorld2* = """import happyx
+
+proc createDom(): TagRef =
+  result = buildHtml:
+    "Hello, world!"
+
+appRoutes "app":
+  "/":
+    createDom
+"""
+  karaxEventModel* = """include karax / prelude
+
+var lines: seq[kstring] = @[]
+
+proc createDom(): VNode =
+  result = buildHtml(tdiv):
+    button:
+      text "Say hello!"
+      proc onclick(ev: Event; n: VNode) =
+        lines.add "Hello simulated universe"
+    for x in lines:
+      tdiv:
+        text x
+
+setRenderer createDom
+"""
+  happyxVsKaraxEventModel* = """import happyx
+
+var lines = remember newSeq[string]()
+
+appRoutes "app":
+  "/":
+    tButton:
+      "Say hello!"
+      @click:
+        lines->add("Hello simulated universe")
+    for x in lines:
+      tDiv:
+        {x}
+"""
+  happyxVsKaraxEventModel1* = """import happyx
+
+appRoutes "app":
+  "/":
+    tButton:
+      "Say hello!"
+      @click:
+        console.log(ev)  # ev is event
+        console.log("Hello, world!")
+      @click(event):
+        console.log(event)  # event is event
+        console.log("Goodbye, world!")
+"""
diff --git a/examples/website/src/ui/code/python.nim b/examples/website/src/ui/code/python.nim
index d7825601e..71a126079 100644
--- a/examples/website/src/ui/code/python.nim
+++ b/examples/website/src/ui/code/python.nim
@@ -1,5 +1,5 @@
 const
-  pythonHelloWorldExample* = """from happyx import Server
+  pythonHelloWorldExample* = """from happyxpy import Server
 
 # Create application
 app = Server('127.0.0.1', 5000)
@@ -41,7 +41,7 @@ def handle(user_id: int):
     print(user_id)
     return {'response': user_id}
 """
-  pythonCustomRouteParamType* = """from happyx import Server, register_route_param_type
+  pythonCustomRouteParamType* = """from happyxpy import Server, register_route_param_type
 
 
 app = Server()
@@ -62,7 +62,7 @@ def handle(data: MyUniqueIdentifier):
 
 app.start()
 """
-  pySsrAdvancedHelloWorld* = """from happyx import Server, JsonResponse, Response
+  pySsrAdvancedHelloWorld* = """from happyxpy import Server, JsonResponse, Response
 
 
 app = Server('127.0.0.1', 5000)
@@ -110,7 +110,7 @@ def test_all():
 
 app.start()
 """
-  pySsrAdditionalRoutes* = """from happyx import Server, HttpRequest
+  pySsrAdditionalRoutes* = """from happyxpy import Server, HttpRequest
 
 
 app = Server("127.0.0.1", 5000)
@@ -177,7 +177,7 @@ def read_users():
 
 app.start()
 """
-  pyMongoDb* = """from happyx import Server
+  pyMongoDb* = """from happyxpy import Server
 from pymongo import MongoClient
 from datetime import datetime
 
@@ -224,7 +224,7 @@ def read_users():
 
 app.start()
 """
-  pySqlalchemy* = """from happyx import Server
+  pySqlalchemy* = """from happyxpy import Server
 from sqlalchemy import create_engine, Column, Integer, DateTime
 from sqlalchemy.ext.declarative import declarative_base
 from sqlalchemy.orm import sessionmaker
@@ -280,7 +280,7 @@ def read_users():
 
 app.start()
 """
-  pySqlalchemy1* = """from happyx import Server
+  pySqlalchemy1* = """from happyxpy import Server
 from sqlalchemy import create_engine, Column, Integer, DateTime
 from sqlalchemy.ext.declarative import declarative_base
 from sqlalchemy.orm import sessionmaker
@@ -337,7 +337,7 @@ def read_users():
 app.start()
 """
   pyPostgreSql* = """import psycopg2
-from happyx import Server
+from happyxpy import Server
 from datetime import datetime
 
 # Connect to postgresql
@@ -407,7 +407,7 @@ def read_users():
 app.start()
 """
   pyPostgreSql1* = """import psycopg2
-from happyx import Server
+from happyxpy import Server
 from datetime import datetime
 
 # Connect to postgresql
@@ -473,7 +473,7 @@ def read_users():
     return response
 
 app.start()"""
-  pyMounting* = """from happyx import Server
+  pyMounting* = """from happyxpy import Server
 
 app = Server()
 profile = Server()
diff --git a/examples/website/src/ui/translations.nim b/examples/website/src/ui/translations.nim
index 1669eda6a..7ae35e1b2 100644
--- a/examples/website/src/ui/translations.nim
+++ b/examples/website/src/ui/translations.nim
@@ -2144,6 +2144,78 @@ translatable:
     "zh" -> "使用Nim，您还可以使用语法糖，无需挂载即可定义单独的路由："
     "fr" -> "En utilisant Nim, vous pouvez également utiliser du sucre syntaxique qui vous permet de définir des routes séparées sans avoir recours au montage :"
     "ko" -> "Nim을 사용하면 마운팅을 사용하지 않고 별도의 경로를 정의할 수 있는 문법적 설탕도 사용할 수 있습니다:"
+  "HappyX for ...":
+    "ru" -> "HappyX для ..."
+    "ja" -> "HappyXのための..."
+    "zh" -> "HappyX的..."
+    "fr" -> "HappyX pour ..."
+    "ko" -> "...를 위한 HappyX"
+  "Karax users 👑":
+    "ru" -> "пользователей Karax 👑"
+    "ja" -> "Karaxユーザー 👑"
+    "zh" -> "Karax用户 👑"
+    "fr" -> "utilisateurs de Karax 👑"
+    "ko" -> "Karax 사용자 👑"
+  "HappyX for Karax users 👑":
+    "ru" -> "HappyX для пользователей Karax 👑"
+    "ja" -> "KaraxユーザーのためのHappyX 👑"
+    "zh" -> "Karax用户的HappyX 👑"
+    "fr" -> "HappyX pour les utilisateurs de Karax 👑"
+    "ko" -> "Karax 사용자를 위한 HappyX 👑"
+  "This article discusses the differences between Karax and HappyX, as well as their pros and cons.":
+    "ru" -> "В этой статье рассматривается разница между Karax и HappyX, а также их плюсы и минусы."
+    "ja" -> "この記事では、KaraxとHappyXの違い、およびそれぞれの長所と短所について説明します。"
+    "zh" -> "本文讨论了Karax和HappyX之间的差异，以及它们的优缺点。"
+    "fr" -> "Cet article aborde les différences entre Karax et HappyX, ainsi que leurs avantages et inconvénients."
+    "ko" -> "이 기사에서는 Karax와 HappyX의 차이점과 장단점에 대해 설명합니다."
+  "I propose to consider the first example from the Karax README:":
+    "ru" -> "Предлагаю рассмотреть первый пример из README Karax:"
+    "ja" -> "Karax READMEの最初の例を検討しましょう："
+    "zh" -> "我建议考虑Karax README中的第一个示例："
+    "fr" -> "Je propose d'examiner le premier exemple du README de Karax :"
+    "ko" -> "Karax README의 첫 번째 예제를 살펴보겠습니다:"
+  "Here's how you can rewrite it using HappyX:":
+    "ru" -> "Вот, как это можно переписать, используя HappyX:"
+    "ja" -> "これをHappyXを使用して書き直す方法は次のとおりです："
+    "zh" -> "以下是使用HappyX重写它的方法："
+    "fr" -> "Voici comment le réécrire en utilisant HappyX :"
+    "ko" -> "다음은 HappyX를 사용하여 이를 다시 작성하는 방법입니다:"
+  "However, if you look again, you can see that the Karax example uses a function that returns VDOM.":
+    "ru" -> "Однако, если взглянуть еще раз - можно увидеть, что в примере с Karax используется функция, которая возвращает VDOM."
+    "ja" -> "しかし、もう一度見ると、Karaxの例ではVDOMを返す関数が使用されていることがわかります。"
+    "zh" -> "然而，如果再看一遍，你会发现Karax示例使用了一个返回VDOM的函数。"
+    "fr" -> "Cependant, si vous regardez à nouveau, vous pouvez voir que l'exemple de Karax utilise une fonction qui renvoie un VDOM."
+    "ko" -> "그러나 다시 보면 Karax 예제에서는 VDOM을 반환하는 함수를 사용하고 있음을 알 수 있습니다."
+  "In HappyX, you can also use functions to generate VDOM:":
+    "ru" -> "В HappyX также можно использовать функции для генерации VDOM:"
+    "ja" -> "HappyXでは、VDOMを生成するために関数を使用することもできます："
+    "zh" -> "在HappyX中，你也可以使用函数来生成VDOM："
+    "fr" -> "Dans HappyX, vous pouvez également utiliser des fonctions pour générer un VDOM :"
+    "ko" -> "HappyX에서는 VDOM을 생성하기 위해 함수도 사용할 수 있습니다:"
+  "The event model":
+    "ru" -> "Модель событий"
+    "ja" -> "イベントモデル"
+    "zh" -> "事件模型"
+    "fr" -> "Le modèle d'événement"
+    "ko" -> "이벤트 모델"
+  "Now let's consider the event model provided by HappyX and Karax.":
+    "ru" -> "Теперь рассмотрим модель событий, которую предоставляют HappyX и Karax."
+    "ja" -> "さて、HappyXとKaraxが提供するイベントモデルについて考えてみましょう。"
+    "zh" -> "现在让我们来看看HappyX和Karax提供的事件模型。"
+    "fr" -> "Maintenant, examinons le modèle d'événement proposé par HappyX et Karax."
+    "ko" -> "이제 HappyX와 Karax가 제공하는 이벤트 모델을 살펴보겠습니다."
+  "Unlike Karax, HappyX does not change the event model and allows you to embed as many events as you like on a single element.":
+    "ru" -> "В отличие от Karax, HappyX не изменяет модель событий и позволяет вам встраивать сколько угодно событий на один элемент."
+    "ja" -> "Karaxとは異なり、HappyXはイベントモデルを変更せず、1つの要素に複数のイベントを埋め込むことができます。"
+    "zh" -> "与Karax不同，HappyX不会改变事件模型，并允许您在单个元素上嵌入任意多的事件。"
+    "fr" -> "Contrairement à Karax, HappyX ne modifie pas le modèle d'événement et vous permet d'intégrer autant d'événements que vous le souhaitez sur un seul élément."
+    "ko" -> "Karax와는 달리 HappyX는 이벤트 모델을 변경하지 않으며 하나의 요소에 여러 이벤트를 임베드할 수 있습니다."
+  "You are also free to change the variable name responsible for the event:":
+    "ru" -> "Вы также вольны изменять название переменной, отвечающей за событие:"
+    "ja" -> "イベントに責任を持つ変数名を自由に変更することもできます："
+    "zh" -> "您还可以自由更改负责事件的变量名称："
+    "fr" -> "Vous êtes également libre de changer le nom de la variable responsable de l'événement :"
+    "ko" -> "이벤트를 처리하는 변수 이름을 자유롭게 변경할 수도 있습니다:"
 
 
 var spokenLang: cstring
diff --git a/src/happyx/spa/components.nim b/src/happyx/spa/components.nim
index eee6811fc..edbf8098b 100644
--- a/src/happyx/spa/components.nim
+++ b/src/happyx/spa/components.nim
@@ -80,22 +80,6 @@ template reRenderTmpl*() =
   compTmpData.addArgIter("data-" & self.uniqCompId)
   # echo tmpData
   when defined(js):
-    #   styleE = compTmpData.lastChild.cloneNode(true)
-    #   newE = current.cloneNode(true)
-    #   parent = current.parentElement
-    #   idx = index(current)
-    #   length = parent.children.len
-    # echo idx, ", ", length
-    # current.remove()
-    # compTmpData.lastChild.remove()
-    # if length-1 == idx:
-    #   parent.appendChild(compTmpData)
-    # else:
-    #   parent.insertBefore(compTmpData, parent.children[idx])
-    # compTmpData.appendChild(styleE)
-    # current.appendChild()
-    # current.innerHTML = ""
-    # current.appendChild()
     var
       current = document.querySelector("[data-" & self.uniqCompId & "]")
       elements = newSeq[Element]()
@@ -117,6 +101,7 @@ template reRenderTmpl*() =
         parent.appendChild(tag)
       else:
         parent.insertBefore(tag, parent.childNodes[idx])
+    # Save focus
     if activeElement.hasAttribute("id"):
       let actElem = document.getElementById(activeElement.id)
       if not actElem.isNil:
@@ -191,7 +176,7 @@ macro component*(name, body: untyped): untyped =
   ##      requiredField: int
   ##      optionalField: int = 100
   ##      
-  ##      `template`:
+  ##      html:
   ##        tDiv:
   ##          "requiredField is {self.requiredField}"
   ##        tDiv:
@@ -201,8 +186,7 @@ macro component*(name, body: untyped): untyped =
   ##        echo self.requiredField
   ##        echo self.optionalField
   ##      
-  ##      `style`:
-  ##        """
+  ##      `style`: """
   ##        div {
   ##          width: {self.requiredField}px;
   ##          height: {self.optionalField}px;
@@ -989,7 +973,8 @@ macro component*(name, body: untyped): untyped =
 macro importComponent*(body: untyped): untyped =
   ## Imports `.hpx` file as component.
   ## 
-  ##   Note: You can easily create these files with HappyX VS Code extension
+  ## .. Note::
+  ##    You can easily create these files with HappyX VS Code extension
   ## 
   ## ## Example
   ## 
@@ -1001,10 +986,12 @@ macro importComponent*(body: untyped): untyped =
   ##        Hello, world!
   ##      </div>
   ##    </template>
+  ##    
   ##    <script>
   ##    # Here is Nim code
   ##    echo "Hello, world!"
   ##    </script>
+  ##    
   ##    <style>
   ##    /* Here is scoped style */
   ##    div {
@@ -1287,4 +1274,4 @@ macro importComponent*(body: untyped): untyped =
     importStmts,
     newNimNode(nnkCommand).add(ident"component", componentName, stmtList)
   )
-  echo result.toStrLit
+  # echo result.toStrLit
diff --git a/src/happyx/spa/renderer.nim b/src/happyx/spa/renderer.nim
index 2bf3e992a..6c21d140d 100644
--- a/src/happyx/spa/renderer.nim
+++ b/src/happyx/spa/renderer.nim
@@ -507,6 +507,8 @@ template thunkHtml*(body: untyped): proc(): TagRef =
   proc(): TagRef =
     result = buildHtml:
       body
+    result = result.children[0].TagRef
+    result.onlyChildren = true
 
 
 template thunkHtmls*(body: untyped): seq[proc(): TagRef] =
@@ -517,6 +519,8 @@ template thunkHtmls*(body: untyped): seq[proc(): TagRef] =
         proc(): TagRef =
           result = buildHtml:
             b
+          result = result.children[0].TagRef
+          result.onlyChildren = true
     body
     res
 
@@ -528,6 +532,8 @@ template buildHtmls*(body: untyped): seq[TagRef] =
       res.add:
         buildHtml:
           b
+      res[^1] = res[^1].children[0].TagRef
+      res[^1].onlyChildren = true
     body
     res
 
diff --git a/src/happyx/spa/tag.nim b/src/happyx/spa/tag.nim
index 582c76f23..0f7f19d48 100644
--- a/src/happyx/spa/tag.nim
+++ b/src/happyx/spa/tag.nim
@@ -166,7 +166,7 @@ proc add*(self: TagRef, tags: varargs[TagRef]) =
       if tag.isNil():
         continue
       if tag.onlyChildren:
-        for i in tag.childNodes[0..^1]:
+        for i in tag.childNodes:
           self.add(i.TagRef)
       else:
         self.appendChild(tag.cloneNode(true))
diff --git a/src/happyx/spa/translatable.nim b/src/happyx/spa/translatable.nim
index 6ff667d70..7c754da6a 100644
--- a/src/happyx/spa/translatable.nim
+++ b/src/happyx/spa/translatable.nim
@@ -27,6 +27,12 @@ type
     lang*: string
 
 
+template i18n*(body: untyped): untyped =
+  ## Shorthand for `translatable macro<#translatable.m,untyped>`_
+  translatable:
+    body
+
+
 macro translatable*(body: untyped): untyped =
   ## Make translations for strings
   ## 
@@ -135,6 +141,16 @@ macro translate*(self: string, variables: varargs[string]): string =
   ## 
   ## .. Note::
   ##    in JS backend this works like procedure calling.
+  ## 
+  ## ### Example
+  ## ```nim
+  ## translatable:
+  ##   "hello_world":
+  ##     "default" -> "Hello, $1th world!"
+  ##     "ru" -> "Привет, $1ый мир!"
+  ## 
+  ## echo translate("hello_world", "10")  # Hello, 10th world!
+  ## ```
   when defined(js):
     return newCall("translateImpl", self, variables)
   else:


Test patch:
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 212d1b64e..6e80571a6 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -41,7 +41,7 @@ jobs:
           - ubuntu-latest
         nim_version:
           - '2.0.0'
-          - '2.0.4'
+          - '2.0.6'
     env:
       TIMEOUT_EXIT_STATUS: 124
     steps:
diff --git a/bindings/python/test1.py b/bindings/python/test1.py
index 489467469..4b1f1cbfe 100644
--- a/bindings/python/test1.py
+++ b/bindings/python/test1.py
@@ -1,4 +1,4 @@
-from happyx import (
+from happyxpy import (
     FileResponse, HtmlResponse, JsonResponse, HttpRequest, WebSocket,
     Response, Server, reg_cors, RequestModelBase, __version__
 )
diff --git a/bindings/python/test2.py b/bindings/python/test2.py
index d6575a83f..77fd573f7 100644
--- a/bindings/python/test2.py
+++ b/bindings/python/test2.py
@@ -1,4 +1,4 @@
-from happyx import new_server, register_route_param_type
+from happyxpy import new_server, register_route_param_type
 
 
 app = new_server("127.0.0.1", 5000)
diff --git a/tests/testjs18.nim b/tests/testjs18.nim
index 8d6f5b5c6..2e3c05561 100644
--- a/tests/testjs18.nim
+++ b/tests/testjs18.nim
@@ -24,18 +24,6 @@ let promise = withPromise res:
     {.emit: "res(true)".}
 
 
-let html0 = htmlProcs[0]
-var x = buildHtml:
-    tSpan:
-      html0
-      {html0().onlyChildren}
-echo "X: ", x
-
-echo htmlTags[0]
-echo htmlTags[0].onlyChildren
-echo htmlTags[0].children[0]
-echo htmlTags[0].children[0].TagRef.onlyChildren
-
 component FormatProcHtml:
   p: (proc (): TagRef) # parentheses needed in avoid "nested statements" error
   html:
@@ -50,8 +38,6 @@ component FormatTagHtml:
 
 appRoutes("app"):
   "/":
-    tDiv: {htmlProcs[0]()}
-    tDiv: html0
     for i in 1..5:
       tDiv:{$htmlProcs[0]()}
       FormatProcHtml(htmlProcs[1])