Skip to content

Commit 1e40a53

Browse files
authored
Merge pull request #581 from callmephilip/fix/html2ft-edge-cases
Fix html2ft edge cases, group custom attrs, add tests
2 parents 131b5a5 + 5ba7038 commit 1e40a53

File tree

2 files changed

+78
-23
lines changed

2 files changed

+78
-23
lines changed

fasthtml/components.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -189,17 +189,23 @@ def _parse(elm, lvl=0, indent=4):
189189
cts = elm.contents
190190
cs = [repr(c.strip()) if isinstance(c, str) else _parse(c, lvl+1)
191191
for c in cts if str(c).strip()]
192-
attrs = []
192+
attrs, exotic_attrs = [], {}
193193
for key, value in sorted(elm.attrs.items(), key=lambda x: x[0]=='class'):
194194
if isinstance(value,(tuple,list)): value = " ".join(value)
195-
key = rev_map.get(key, key)
196-
attrs.append(f'{key.replace("-", "_")}={value!r}'
197-
if _re_h2x_attr_key.match(key) else f'**{{{key!r}:{value!r}}}')
195+
key, value = rev_map.get(key, key), value or True
196+
if _re_h2x_attr_key.match(key): attrs.append(f'{key.replace("-", "_")}={value!r}')
197+
else: exotic_attrs[key] = value
198+
if exotic_attrs: attrs.append(f'**{exotic_attrs!r}')
198199
spc = " "*lvl*indent
199200
onlychild = not cts or (len(cts)==1 and isinstance(cts[0],str))
200201
j = ', ' if onlychild else f',\n{spc}'
201202
inner = j.join(filter(None, cs+attrs))
202-
if onlychild: return f'{tag_name}({inner})'
203+
if onlychild:
204+
if not attr1st: return f'{tag_name}({inner})'
205+
else:
206+
# respect attr1st setting
207+
attrs = ', '.join(filter(None, attrs))
208+
return f'{tag_name}({attrs})({cs[0] if cs else ""})'
203209
if not attr1st or not attrs: return f'{tag_name}(\n{spc}{inner}\n{" "*(lvl-1)*indent})'
204210
inner_cs = j.join(filter(None, cs))
205211
inner_attrs = ', '.join(filter(None, attrs))

nbs/api/01_components.ipynb

Lines changed: 67 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -738,17 +738,23 @@
738738
" cts = elm.contents\n",
739739
" cs = [repr(c.strip()) if isinstance(c, str) else _parse(c, lvl+1)\n",
740740
" for c in cts if str(c).strip()]\n",
741-
" attrs = []\n",
741+
" attrs, exotic_attrs = [], {}\n",
742742
" for key, value in sorted(elm.attrs.items(), key=lambda x: x[0]=='class'):\n",
743743
" if isinstance(value,(tuple,list)): value = \" \".join(value)\n",
744-
" key = rev_map.get(key, key)\n",
745-
" attrs.append(f'{key.replace(\"-\", \"_\")}={value!r}'\n",
746-
" if _re_h2x_attr_key.match(key) else f'**{{{key!r}:{value!r}}}')\n",
744+
" key, value = rev_map.get(key, key), value or True\n",
745+
" if _re_h2x_attr_key.match(key): attrs.append(f'{key.replace(\"-\", \"_\")}={value!r}')\n",
746+
" else: exotic_attrs[key] = value\n",
747+
" if exotic_attrs: attrs.append(f'**{exotic_attrs!r}')\n",
747748
" spc = \" \"*lvl*indent\n",
748749
" onlychild = not cts or (len(cts)==1 and isinstance(cts[0],str))\n",
749750
" j = ', ' if onlychild else f',\\n{spc}'\n",
750751
" inner = j.join(filter(None, cs+attrs))\n",
751-
" if onlychild: return f'{tag_name}({inner})'\n",
752+
" if onlychild:\n",
753+
" if not attr1st: return f'{tag_name}({inner})'\n",
754+
" else:\n",
755+
" # respect attr1st setting\n",
756+
" attrs = ', '.join(filter(None, attrs))\n",
757+
" return f'{tag_name}({attrs})({cs[0] if cs else \"\"})'\n",
752758
" if not attr1st or not attrs: return f'{tag_name}(\\n{spc}{inner}\\n{\" \"*(lvl-1)*indent})' \n",
753759
" inner_cs = j.join(filter(None, cs))\n",
754760
" inner_attrs = ', '.join(filter(None, attrs))\n",
@@ -816,18 +822,18 @@
816822
"```python\n",
817823
"Form(\n",
818824
" Fieldset(name='stuff')(\n",
819-
" Input(value='Profit', id='title', name='title', cls='char'),\n",
825+
" Input(value='Profit', id='title', name='title', cls='char')(),\n",
820826
" Label(cls='px-2')(\n",
821-
" Input(type='checkbox', name='done', data_foo='bar', checked='1', cls='checkboxer'),\n",
827+
" Input(type='checkbox', name='done', data_foo='bar', checked='1', cls='checkboxer')(),\n",
822828
" 'Done'\n",
823829
" ),\n",
824-
" Input(type='hidden', id='id', name='id', value='2'),\n",
830+
" Input(type='hidden', id='id', name='id', value='2')(),\n",
825831
" Select(name='opt')(\n",
826-
" Option(value='a'),\n",
827-
" Option(value='b', selected='1')\n",
832+
" Option(value='a')(),\n",
833+
" Option(value='b', selected='1')()\n",
828834
" ),\n",
829-
" Textarea('Details', id='details', name='details'),\n",
830-
" Button('Save')\n",
835+
" Textarea(id='details', name='details')('Details'),\n",
836+
" Button()('Save')\n",
831837
" )\n",
832838
")\n",
833839
"```"
@@ -885,30 +891,73 @@
885891
},
886892
{
887893
"cell_type": "markdown",
888-
"id": "474e14b4",
894+
"id": "defc22f0",
889895
"metadata": {},
890896
"source": [
891-
"# Export -"
897+
"## Tests"
892898
]
893899
},
894900
{
895901
"cell_type": "code",
896902
"execution_count": null,
897-
"id": "d211e8e2",
903+
"id": "46083529",
898904
"metadata": {},
899905
"outputs": [],
900906
"source": [
901907
"#|hide\n",
902-
"import nbdev; nbdev.nbdev_export()"
908+
"def test_html2ft(html: str, attr1st=False):\n",
909+
" # html -> ft -> html\n",
910+
" assert html == to_xml(eval(html2ft(html, attr1st))).strip()"
903911
]
904912
},
905913
{
906914
"cell_type": "code",
907915
"execution_count": null,
908-
"id": "814cf69a",
916+
"id": "517bcd86",
909917
"metadata": {},
910918
"outputs": [],
911-
"source": []
919+
"source": [
920+
"test_html2ft('<input value=\"Profit\" name=\"title\" id=\"title\" class=\"char\">', attr1st=True)\n",
921+
"test_html2ft('<input value=\"Profit\" name=\"title\" id=\"title\" class=\"char\">')\n",
922+
"test_html2ft('<div id=\"foo\"></div>')\n",
923+
"test_html2ft('<div id=\"foo\">hi</div>')\n",
924+
"test_html2ft('<div x-show=\"open\" x-transition:enter=\"transition duration-300\" x-transition:enter-start=\"opacity-0 scale-90\">Hello 👋</div>')\n",
925+
"test_html2ft('<div x-transition:enter.scale.80 x-transition:leave.scale.90>hello</div>')"
926+
]
927+
},
928+
{
929+
"cell_type": "code",
930+
"execution_count": null,
931+
"id": "99773c69",
932+
"metadata": {},
933+
"outputs": [],
934+
"source": [
935+
"assert html2ft('<div id=\"foo\">hi</div>', attr1st=True) == \"Div(id='foo')('hi')\"\n",
936+
"assert html2ft(\"\"\"\n",
937+
" <div x-show=\"open\" x-transition:enter=\"transition duration-300\" x-transition:enter-start=\"opacity-0 scale-90\">Hello 👋</div>\n",
938+
"\"\"\") == \"Div('Hello 👋', x_show='open', **{'x-transition:enter': 'transition duration-300', 'x-transition:enter-start': 'opacity-0 scale-90'})\"\n",
939+
"assert html2ft('<div x-transition:enter.scale.80 x-transition:leave.scale.90>hello</div>') == \"Div('hello', **{'x-transition:enter.scale.80': True, 'x-transition:leave.scale.90': True})\"\n",
940+
"assert html2ft(\"<img alt=' ' />\") == \"Img(alt=' ')\""
941+
]
942+
},
943+
{
944+
"cell_type": "markdown",
945+
"id": "474e14b4",
946+
"metadata": {},
947+
"source": [
948+
"# Export -"
949+
]
950+
},
951+
{
952+
"cell_type": "code",
953+
"execution_count": null,
954+
"id": "d211e8e2",
955+
"metadata": {},
956+
"outputs": [],
957+
"source": [
958+
"#|hide\n",
959+
"import nbdev; nbdev.nbdev_export()"
960+
]
912961
}
913962
],
914963
"metadata": {

0 commit comments

Comments
 (0)