diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index d8e35b6..e74d299 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -1,30 +1,40 @@
-name: Upload Python Package to PyPI when a Release is Created
+name: Publish to PyPI
on:
- release:
- types: [created]
+ release:
+ types: [published]
jobs:
- pypi-publish:
- name: Publish release to PyPI
- runs-on: ubuntu-latest
- environment:
- name: pypi
- url: https://pypi.org/p/Better-MD
- permissions:
- id-token: write
- steps:
- - uses: actions/checkout@v4
- - name: Set up Python
- uses: actions/setup-python@v4
- with:
- python-version: "3.x"
- - name: Install dependencies
- run: |
- python -m pip install --upgrade pip
- pip install setuptools wheel
- - name: Build package
- run: |
- python setup.py sdist bdist_wheel
- - name: Publish package distributions to PyPI
- uses: pypa/gh-action-pypi-publish@release/v1
+ publish:
+ runs-on: ubuntu-latest
+ permissions:
+ id-token: write
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: '3.x'
+
+ - name: Install dependencies
+ run: |
+ python -m pip install --upgrade pip
+ pip install build twine
+
+ - name: Build package
+ run: python -m build
+
+ - name: Publish to PyPI
+ uses: pypa/gh-action-pypi-publish@v1.8.11
+ with:
+ user: __token__
+ password: ${{ secrets.PYPI_API_TOKEN }}
+
+ - name: Publish to Test PyPI
+ uses: pypa/gh-action-pypi-publish@v1.8.11
+ with:
+ user: __token__
+ password: ${{ secrets.PYPI_API_TOKEN }}
+ repository_url: https://test.pypi.org/legacy/
diff --git a/BetterMD/__init__.py b/BetterMD/__init__.py
index ca4f4ac..416e17f 100644
--- a/BetterMD/__init__.py
+++ b/BetterMD/__init__.py
@@ -1,4 +1,48 @@
+import logging
from .elements import *
from .html import CustomHTML
from .markdown import CustomMarkdown
-from .rst import CustomRst
\ No newline at end of file
+from .rst import CustomRst
+from .parse import HTMLParser, MDParser, Collection
+
+class HTML:
+ @staticmethod
+ def from_string(html:'str'):
+ return Symbol.from_html(html)
+
+ @staticmethod
+ def from_file(file):
+ return Symbol.from_html(file)
+
+ @staticmethod
+ def from_url(url):
+ import requests as r
+ text = r.get(url).text
+
+ if text.startswith(""):
+ text = text[15:]
+
+ return Symbol.from_html(text)
+
+class MD:
+ @staticmethod
+ def from_string(md:'str'):
+ return Symbol.from_md(md)
+
+ @staticmethod
+ def from_file(file):
+ return Symbol.from_md(file)
+
+ @staticmethod
+ def from_url(url):
+ import requests as r
+ text = r.get(url).text
+ return Symbol.from_md(text)
+
+def enable_debug_mode():
+ logging.basicConfig(level=logging.DEBUG)
+ logger = logging.getLogger("BetterMD")
+
+ return logger
+
+__all__ = ["HTML", "MD", "Symbol", "Collection", "HTMLParser", "MDParser", "CustomHTML", "CustomMarkdown", "CustomRst", "enable_debug_mode"]
diff --git a/BetterMD/__main__.py b/BetterMD/__main__.py
new file mode 100644
index 0000000..cda04ae
--- /dev/null
+++ b/BetterMD/__main__.py
@@ -0,0 +1,10 @@
+import logging
+
+
+def setup_logger():
+ LEVEL = logging.INFO
+ logging.basicConfig(level=LEVEL)
+ logger = logging.getLogger("BetterMD")
+ return logger
+
+setup_logger()
\ No newline at end of file
diff --git a/BetterMD/elements/__init__.py b/BetterMD/elements/__init__.py
index 0be6ba9..5d6d381 100644
--- a/BetterMD/elements/__init__.py
+++ b/BetterMD/elements/__init__.py
@@ -1,16 +1,127 @@
+from .symbol import Symbol
+from .comment import Comment
+from .svg import *
+
+from .text_formatting import Strong, Em, B
+
from .a import A
+from .abbr import Abbr
+from .acronym import Acronym
+from .address import Address
+from .area import Area
+from .article import Article
+from .aside import Aside
+from .audio import Audio
+
+from .base import Base
+from .bd import BDI, BDO
+from .big import Big
+from .blockquote import Blockquote
+from .body import Body
+from .br import Br
+from .button import Button
+
+from .canvas import Canvas
+from .caption import Caption
+from .center import Center
+from .cite import Cite
+from .code import Code
+from .col import Col, Colgroup
+
+from .d import DD, DFN, DL, DT
+from .data import Data
+from .datalist import DataList
+from .del_ import Del # Using _ to avoid conflict with del keyword
+from .details import Details
+from .dialog import Dialog
+from .dir import Dir
+from .div import Div
+
+from .embed import Embed
+
+from .fencedframe import FencedFrame
+from .fieldset import Fieldset
+from .figure import FigCaption, Figure
+from .font import Font
+from .footer import Footer
+from .form import Form
+from .frame import Frame
+from .frameset import Frameset
+
from .h import H1,H2,H3,H4,H5,H6
from .head import Head
+from .header import Header
+from .hgroup import HGroup
+from .hr import Hr
+from .html import HTML
+
+from .i import I
+from .iframe import Iframe
+from .img import Img
+from .input import Input
+from .ins import Ins
+
+from .kbd import Kbd
+
+from .label import Label
+from .legend import Legend
from .li import OL, UL, LI
-from .text import Text
-from .div import Div
+from .link import Link
+
+from .main import Main
+from .map import Map
+from .mark import Mark
+from .marquee import Marquee
+from .menu import Menu
+from .meta import Meta
+from .meter import Meter
+
+from .nav import Nav
+from .no import NoFrames, NoScript, NoBr, NoEmbed
+
+from .object import Object
+from .output import Output
+
from .p import P
+from .param import Param
+from .picture import Picture
+from .plaintext import Plaintext
+from .progress import Progress
+
+from .q import Q
+
+from .ruby import RB, RP, RT, RTC
+
+from .s import S
+from .samp import Samp
+from .script import Script
+from .search import Search
+from .section import Section
+from .select import Select
+from .slot import Slot
+from .small import Small
+from .source import Source
from .span import Span
-from .img import Img
-from .text_formatting import Strong, Em, Code
-from .br import Br
-from .blockquote import Blockquote
-from .hr import Hr
-from .table import Table, Tr, Td, Th
-from .input import Input
-from .code import Code
\ No newline at end of file
+from .strike import Strike
+from .style import Style
+from .sub import Sub
+from .summary import Summary
+from .sup import Sup
+
+from .table import Table, Tr, Td, Th, THead, TBody, TFoot
+from .template import Template
+from .text import Text
+from .textarea import Textarea
+from .time import Time
+from .title import Title
+from .track import Track
+from .tt import TT
+
+from .u import U
+
+from .var import Var
+from .video import Video
+
+from .wbr import WBR
+
+from .xmp import XMP
\ No newline at end of file
diff --git a/BetterMD/elements/a.py b/BetterMD/elements/a.py
index f9364a4..de2c8b9 100644
--- a/BetterMD/elements/a.py
+++ b/BetterMD/elements/a.py
@@ -1,23 +1,27 @@
-from BetterMD.rst.custom_rst import CustomRst
from .symbol import Symbol
+from ..rst import CustomRst
from ..markdown import CustomMarkdown
-from ..html import CustomHTML
-import typing as t
-class MD(CustomMarkdown['A']):
+class MD(CustomMarkdown):
def to_md(self, inner, symbol, parent):
return f"[{" ".join([e.to_md() for e in inner])}]({symbol.get_prop("href")})"
-class HTML(CustomHTML['A']):
- def to_html(self, inner, symbol, parent):
- return f"{" ".join([e.to_html() for e in inner])}"
-
class RST(CustomRst['A']):
def to_rst(self, inner, symbol, parent):
return f"`{' '.join([e.to_rst() for e in inner])} <{symbol.get_prop('href')}>`_"
class A(Symbol):
prop_list = ["href"]
+
+ refs = {}
md = MD()
- html = HTML()
- rst = RST()
\ No newline at end of file
+ html = "a"
+ rst = RST()
+
+ @classmethod
+ def get_ref(cls, name):
+ return cls.refs[name]
+
+ @classmethod
+ def email(cls, email):
+ return cls(href=f"mailto:{email}")
\ No newline at end of file
diff --git a/BetterMD/elements/abbr.py b/BetterMD/elements/abbr.py
new file mode 100644
index 0000000..24f8403
--- /dev/null
+++ b/BetterMD/elements/abbr.py
@@ -0,0 +1,8 @@
+from .symbol import Symbol
+
+class Abbr(Symbol):
+ prop_list = ["title"]
+
+ md = ""
+ html = "abbr"
+ rst = ""
\ No newline at end of file
diff --git a/BetterMD/elements/acronym.py b/BetterMD/elements/acronym.py
new file mode 100644
index 0000000..d23d6bd
--- /dev/null
+++ b/BetterMD/elements/acronym.py
@@ -0,0 +1,8 @@
+from .symbol import Symbol
+
+class Acronym(Symbol):
+ prop_list = ["title"]
+
+ md = ""
+ html = "acronym"
+ rst = ""
\ No newline at end of file
diff --git a/BetterMD/elements/address.py b/BetterMD/elements/address.py
new file mode 100644
index 0000000..d06bb3d
--- /dev/null
+++ b/BetterMD/elements/address.py
@@ -0,0 +1,6 @@
+from .symbol import Symbol
+
+class Address(Symbol):
+ md = ""
+ html = "address"
+ rst = ""
\ No newline at end of file
diff --git a/BetterMD/elements/area.py b/BetterMD/elements/area.py
new file mode 100644
index 0000000..5f3b02a
--- /dev/null
+++ b/BetterMD/elements/area.py
@@ -0,0 +1,8 @@
+from .symbol import Symbol
+
+class Area(Symbol):
+ prop_list = ["alt", "coords", "download", "href", "ping", "referrerpolicy", "rel", "shape", "target"]
+
+ md = ""
+ html = "area"
+ rst = ""
\ No newline at end of file
diff --git a/BetterMD/elements/article.py b/BetterMD/elements/article.py
new file mode 100644
index 0000000..b9a6214
--- /dev/null
+++ b/BetterMD/elements/article.py
@@ -0,0 +1,6 @@
+from .symbol import Symbol
+
+class Article(Symbol):
+ md = ""
+ html = "article"
+ rst = ""
\ No newline at end of file
diff --git a/BetterMD/elements/aside.py b/BetterMD/elements/aside.py
new file mode 100644
index 0000000..5316633
--- /dev/null
+++ b/BetterMD/elements/aside.py
@@ -0,0 +1,6 @@
+from .symbol import Symbol
+
+class Aside(Symbol):
+ md = ""
+ html = "aside"
+ rst = ""
\ No newline at end of file
diff --git a/BetterMD/elements/audio.py b/BetterMD/elements/audio.py
new file mode 100644
index 0000000..ea32f57
--- /dev/null
+++ b/BetterMD/elements/audio.py
@@ -0,0 +1,8 @@
+from .symbol import Symbol
+
+class Audio(Symbol):
+ prop_list = ["autoplay", "controls", "crossorigin", "disableremoteplayback", "loop", "muted", "preload", "src"]
+
+ md = ""
+ html = "audio"
+ rst = ""
\ No newline at end of file
diff --git a/BetterMD/elements/base.py b/BetterMD/elements/base.py
new file mode 100644
index 0000000..4d54ea6
--- /dev/null
+++ b/BetterMD/elements/base.py
@@ -0,0 +1,8 @@
+from .symbol import Symbol
+
+class Base(Symbol):
+ prop_list = ["href", "target"]
+
+ md = ""
+ html = "base"
+ rst = ""
\ No newline at end of file
diff --git a/BetterMD/elements/bd.py b/BetterMD/elements/bd.py
new file mode 100644
index 0000000..a550539
--- /dev/null
+++ b/BetterMD/elements/bd.py
@@ -0,0 +1,15 @@
+from .symbol import Symbol
+
+class BDI(Symbol):
+ prop_list = ["dir"]
+
+ md = ""
+ html = "bdi"
+ rst = ""
+
+class BDO(Symbol):
+ prop_list = ["dir"]
+
+ md = ""
+ html = "bdo"
+ rst = ""
\ No newline at end of file
diff --git a/BetterMD/elements/big.py b/BetterMD/elements/big.py
new file mode 100644
index 0000000..583f7c0
--- /dev/null
+++ b/BetterMD/elements/big.py
@@ -0,0 +1,6 @@
+from .symbol import Symbol
+
+class Big(Symbol):
+ md = ""
+ html = "big"
+ rst = ""
\ No newline at end of file
diff --git a/BetterMD/elements/body.py b/BetterMD/elements/body.py
new file mode 100644
index 0000000..8a1aaf3
--- /dev/null
+++ b/BetterMD/elements/body.py
@@ -0,0 +1,7 @@
+from .symbol import Symbol
+
+
+class Body(Symbol):
+ html = "body"
+ md = ""
+ rst = ""
\ No newline at end of file
diff --git a/BetterMD/elements/button.py b/BetterMD/elements/button.py
new file mode 100644
index 0000000..8a698cc
--- /dev/null
+++ b/BetterMD/elements/button.py
@@ -0,0 +1,8 @@
+from .symbol import Symbol
+
+class Button(Symbol):
+ prop_list = ["autofocus", "command", "commandfor", "disabled", "form", "formaction", "formenctype", "formmethod", "formnovalidate", "formtarget", "name", "popovertarget", "popovertargetaction", "type", "value"]
+
+ md = ""
+ html = "button"
+ rst = ""
\ No newline at end of file
diff --git a/BetterMD/elements/canvas.py b/BetterMD/elements/canvas.py
new file mode 100644
index 0000000..e55c9ff
--- /dev/null
+++ b/BetterMD/elements/canvas.py
@@ -0,0 +1,8 @@
+from .symbol import Symbol
+
+class Canvas(Symbol):
+ prop_list = ["height", "moz-opaque", "width"]
+
+ md = ""
+ html = "canvas"
+ rst = ""
\ No newline at end of file
diff --git a/BetterMD/elements/caption.py b/BetterMD/elements/caption.py
new file mode 100644
index 0000000..4d54e98
--- /dev/null
+++ b/BetterMD/elements/caption.py
@@ -0,0 +1,8 @@
+from .symbol import Symbol
+
+class Caption(Symbol):
+ prop_list = ["align"]
+
+ md = ""
+ html = "caption"
+ rst = ""
\ No newline at end of file
diff --git a/BetterMD/elements/center.py b/BetterMD/elements/center.py
new file mode 100644
index 0000000..9836ed9
--- /dev/null
+++ b/BetterMD/elements/center.py
@@ -0,0 +1,6 @@
+from .symbol import Symbol
+
+class Center(Symbol):
+ md = ""
+ html = "center"
+ rst = ""
\ No newline at end of file
diff --git a/BetterMD/elements/cite.py b/BetterMD/elements/cite.py
new file mode 100644
index 0000000..d7d226c
--- /dev/null
+++ b/BetterMD/elements/cite.py
@@ -0,0 +1,6 @@
+from .symbol import Symbol
+
+class Cite(Symbol):
+ md = ""
+ html = "cite"
+ rst = ""
\ No newline at end of file
diff --git a/BetterMD/elements/code.py b/BetterMD/elements/code.py
index 9403839..31da77d 100644
--- a/BetterMD/elements/code.py
+++ b/BetterMD/elements/code.py
@@ -2,6 +2,7 @@
from .text import Text
from ..markdown import CustomMarkdown
from ..html import CustomHTML
+from ..rst import CustomRst
class MD(CustomMarkdown):
def to_md(self, inner, symbol, parent):
@@ -11,25 +12,57 @@ def to_md(self, inner, symbol, parent):
# If it's a code block (has language or multiline)
if language or "\n" in inner:
- return f"```{language}\n{inner}\n```\n"
-
+ return f"\n```{language}\n{inner}\n```\n" # Cant use block as inline code isn't a block
+
# Inline code
return f"`{inner}`"
class HTML(CustomHTML):
def to_html(self, inner, symbol, parent):
language = symbol.get_prop("language", "")
- if isinstance(inner, Text):
- inner = inner.to_html()
+ inner = "\n".join([i.to_html() for i in inner])
if language:
- return f'
{inner}
'
+ return f'{inner}
'
return f"{inner}
"
+
+ def verify(self, text: str) -> bool:
+ return text.lower() == "code"
+
+class RST(CustomRst):
+ def to_rst(self, inner, symbol, parent):
+ language = symbol.get_prop("language", "")
+
+ # Handle inner content
+ if isinstance(inner, list):
+ content = "".join([
+ i.to_rst() if isinstance(i, Symbol) else str(i)
+ for i in inner
+ ])
+ else:
+ content = inner.to_rst() if isinstance(inner, Symbol) else str(inner)
+
+ # If it's a code block (has language or multiline)
+ if language or "\n" in content:
+ # Use code-block directive for language-specific blocks
+ if language:
+ # Indent the content by 3 spaces (RST requirement)
+ indented_content = "\n".join(f" {line}" for line in content.strip().split("\n"))
+ return f".. code-block:: {language}\n\n{indented_content}\n\n"
+
+ # Use simple literal block for language-less blocks
+ # Indent the content by 3 spaces (RST requirement)
+ indented_content = "\n".join(f" {line}" for line in content.strip().split("\n"))
+ return f"::\n\n{indented_content}\n\n"
+
+ # Inline code
+ # Escape backticks if they exist in content
+ if "`" in content:
+ return f"``{content}``"
+ return f"`{content}`"
class Code(Symbol):
- props = ["language"]
html = HTML()
md = MD()
- rst = "``"
- nl = True
\ No newline at end of file
+ rst = RST()
\ No newline at end of file
diff --git a/BetterMD/elements/col.py b/BetterMD/elements/col.py
new file mode 100644
index 0000000..73f7c9b
--- /dev/null
+++ b/BetterMD/elements/col.py
@@ -0,0 +1,15 @@
+from .symbol import Symbol
+
+class Colgroup(Symbol):
+ prop_list = ["span", "align", "bgcolor", "char", "charoff", "valign", "width"]
+
+ md = ""
+ html = "colgroup"
+ rst = ""
+
+class Col(Symbol):
+ prop_list = ["span", "align", "bgcolor", "char", "charoff", "valign", "width"]
+
+ md = ""
+ html = "col"
+ rst = ""
\ No newline at end of file
diff --git a/BetterMD/elements/comment.py b/BetterMD/elements/comment.py
new file mode 100644
index 0000000..2285a27
--- /dev/null
+++ b/BetterMD/elements/comment.py
@@ -0,0 +1,14 @@
+from .symbol import Symbol
+from ..html import CustomHTML
+
+class HTML(CustomHTML):
+ def to_html(self, inner, symbol, parent):
+ return f""
+
+ def verify(self, text: str) -> bool:
+ return text.lower() == "!--"
+
+class Comment(Symbol):
+ md = ""
+ html = HTML()
+ rst = ""
\ No newline at end of file
diff --git a/BetterMD/elements/d.py b/BetterMD/elements/d.py
new file mode 100644
index 0000000..41962ea
--- /dev/null
+++ b/BetterMD/elements/d.py
@@ -0,0 +1,23 @@
+from .symbol import Symbol
+
+class DD(Symbol):
+ md = ""
+ html = "dd"
+ rst = ""
+
+class DT(Symbol):
+ md = ""
+ html = "dt"
+ rst = ""
+
+class DL(Symbol):
+ md = ""
+ html = "dl"
+ rst = ""
+
+class DFN(Symbol):
+ prop_list = ["title"]
+
+ md = ""
+ html = "dfn"
+ rst = ""
\ No newline at end of file
diff --git a/BetterMD/elements/data.py b/BetterMD/elements/data.py
new file mode 100644
index 0000000..f20226d
--- /dev/null
+++ b/BetterMD/elements/data.py
@@ -0,0 +1,8 @@
+from .symbol import Symbol
+
+class Data(Symbol):
+ prop_list = ["value"]
+
+ md = ""
+ html = "data"
+ rst = ""
\ No newline at end of file
diff --git a/BetterMD/elements/datalist.py b/BetterMD/elements/datalist.py
new file mode 100644
index 0000000..a1f3ce8
--- /dev/null
+++ b/BetterMD/elements/datalist.py
@@ -0,0 +1,6 @@
+from .symbol import Symbol
+
+class DataList(Symbol):
+ md = ""
+ html = "datalist"
+ rst = ""
\ No newline at end of file
diff --git a/BetterMD/elements/del_.py b/BetterMD/elements/del_.py
new file mode 100644
index 0000000..55fbde6
--- /dev/null
+++ b/BetterMD/elements/del_.py
@@ -0,0 +1,8 @@
+from .symbol import Symbol
+
+class Del(Symbol):
+ prop_list = ["cite", "datetime"]
+
+ md = ""
+ html = "del"
+ rst = ""
\ No newline at end of file
diff --git a/BetterMD/elements/details.py b/BetterMD/elements/details.py
new file mode 100644
index 0000000..8fc585c
--- /dev/null
+++ b/BetterMD/elements/details.py
@@ -0,0 +1,9 @@
+from .symbol import Symbol
+
+class Details(Symbol):
+ prop_list = ["open", "name"]
+ event_list = ["toggle"]
+
+ md = ""
+ html = "details"
+ rst = ""
\ No newline at end of file
diff --git a/BetterMD/elements/dialog.py b/BetterMD/elements/dialog.py
new file mode 100644
index 0000000..8d39304
--- /dev/null
+++ b/BetterMD/elements/dialog.py
@@ -0,0 +1,8 @@
+from .symbol import Symbol
+
+class Dialog(Symbol):
+ prop_list = ["open"] # Dont use `tabindex`
+
+ md = ""
+ html = "dialog"
+ rst = ""
\ No newline at end of file
diff --git a/BetterMD/elements/dir.py b/BetterMD/elements/dir.py
new file mode 100644
index 0000000..7a4ff01
--- /dev/null
+++ b/BetterMD/elements/dir.py
@@ -0,0 +1,8 @@
+from .symbol import Symbol
+
+class Dir(Symbol):
+ prop_list = ["compact"]
+
+ md = ""
+ html = "dir"
+ rst = ""
\ No newline at end of file
diff --git a/BetterMD/elements/embed.py b/BetterMD/elements/embed.py
new file mode 100644
index 0000000..9e0fdd9
--- /dev/null
+++ b/BetterMD/elements/embed.py
@@ -0,0 +1,8 @@
+from .symbol import Symbol
+
+class Embed(Symbol):
+ prop_list = ["height", "src", "type", "width"]
+
+ md = ""
+ html = "embed"
+ rst = ""
\ No newline at end of file
diff --git a/BetterMD/elements/fencedframe.py b/BetterMD/elements/fencedframe.py
new file mode 100644
index 0000000..ff222c4
--- /dev/null
+++ b/BetterMD/elements/fencedframe.py
@@ -0,0 +1,8 @@
+from .symbol import Symbol
+
+class FencedFrame(Symbol):
+ prop_list = ["allow", "height", "width"]
+
+ md = ""
+ html = "fencedframe"
+ rst = ""
\ No newline at end of file
diff --git a/BetterMD/elements/fieldset.py b/BetterMD/elements/fieldset.py
new file mode 100644
index 0000000..15eeca4
--- /dev/null
+++ b/BetterMD/elements/fieldset.py
@@ -0,0 +1,8 @@
+from .symbol import Symbol
+
+class Fieldset(Symbol):
+ prop_list = ["disabled", "form", "name"]
+
+ md = ""
+ html = "fieldset"
+ rst = ""
\ No newline at end of file
diff --git a/BetterMD/elements/figure.py b/BetterMD/elements/figure.py
new file mode 100644
index 0000000..2f99e7d
--- /dev/null
+++ b/BetterMD/elements/figure.py
@@ -0,0 +1,11 @@
+from .symbol import Symbol
+
+class Figure(Symbol):
+ md = ""
+ html = "figure"
+ rst = ""
+
+class FigCaption(Symbol):
+ md = ""
+ html = "figcaption"
+ rst = ""
\ No newline at end of file
diff --git a/BetterMD/elements/font.py b/BetterMD/elements/font.py
new file mode 100644
index 0000000..0a90231
--- /dev/null
+++ b/BetterMD/elements/font.py
@@ -0,0 +1,8 @@
+from .symbol import Symbol
+
+class Font(Symbol):
+ prop_list = ["color", "face", "size"]
+
+ md = ""
+ html = "font"
+ rst = ""
\ No newline at end of file
diff --git a/BetterMD/elements/footer.py b/BetterMD/elements/footer.py
new file mode 100644
index 0000000..3d9a802
--- /dev/null
+++ b/BetterMD/elements/footer.py
@@ -0,0 +1,6 @@
+from .symbol import Symbol
+
+class Footer(Symbol):
+ md = ""
+ html = "footer"
+ rst = ""
\ No newline at end of file
diff --git a/BetterMD/elements/form.py b/BetterMD/elements/form.py
new file mode 100644
index 0000000..3928f03
--- /dev/null
+++ b/BetterMD/elements/form.py
@@ -0,0 +1,11 @@
+from .symbol import Symbol
+
+class Form(Symbol):
+ prop_list = [
+ "accept", "accept-charset", "autocapitalize", "autocomplete", "name", "rel",
+ "action", "enctype", "method", "novalidate", "target",
+ ]
+
+ md = ""
+ html = "form"
+ rst = ""
\ No newline at end of file
diff --git a/BetterMD/elements/frame.py b/BetterMD/elements/frame.py
new file mode 100644
index 0000000..3ba1b42
--- /dev/null
+++ b/BetterMD/elements/frame.py
@@ -0,0 +1,10 @@
+from .symbol import Symbol
+
+class Frame(Symbol):
+ prop_list = [
+ "src", "name", "noresize", "scrolling", "marginheight", "marginwidth", "frameborder"
+ ]
+
+ md = ""
+ html = "frame"
+ rst = ""
\ No newline at end of file
diff --git a/BetterMD/elements/frameset.py b/BetterMD/elements/frameset.py
new file mode 100644
index 0000000..840f119
--- /dev/null
+++ b/BetterMD/elements/frameset.py
@@ -0,0 +1,8 @@
+from .symbol import Symbol
+
+class Frameset(Symbol):
+ prop_list = ["cols", "rows"]
+
+ md = ""
+ html = "frameset"
+ rst = ""
\ No newline at end of file
diff --git a/BetterMD/elements/h.py b/BetterMD/elements/h.py
index fa1d01e..d3c1ff1 100644
--- a/BetterMD/elements/h.py
+++ b/BetterMD/elements/h.py
@@ -15,36 +15,36 @@ def to_rst(self, inner: list[Symbol], symbol: Symbol, parent: Symbol) -> str:
class H1(Symbol):
html = "h1"
- md = "#"
+ md = "# "
rst = RST("=")
- nl = True
+ block = True
class H2(Symbol):
html = "h2"
- md = "##"
+ md = "## "
rst = RST("-")
- nl = True
+ block = True
class H3(Symbol):
html = "h3"
- md = "###"
+ md = "### "
rst = RST("~")
- nl = True
+ block = True
class H4(Symbol):
html = "h4"
- md = "####"
+ md = "#### "
rst = RST("+")
- nl = True
+ block = True
class H5(Symbol):
html = "h5"
- md = "#####"
+ md = "##### "
rst = RST("^")
- nl = True
+ block = True
class H6(Symbol):
html = "h6"
- md = "######"
+ md = "###### "
rst = RST('"')
- nl = True
\ No newline at end of file
+ block = True
\ No newline at end of file
diff --git a/BetterMD/elements/head.py b/BetterMD/elements/head.py
index 1f51b31..de3f0e6 100644
--- a/BetterMD/elements/head.py
+++ b/BetterMD/elements/head.py
@@ -1,6 +1,8 @@
from .symbol import Symbol
class Head(Symbol):
+ prop_list = ["profile"]
+
md = ""
html = "head"
rst = ""
\ No newline at end of file
diff --git a/BetterMD/elements/header.py b/BetterMD/elements/header.py
new file mode 100644
index 0000000..af914e9
--- /dev/null
+++ b/BetterMD/elements/header.py
@@ -0,0 +1,6 @@
+from .symbol import Symbol
+
+class Header(Symbol):
+ md = ""
+ html = "header"
+ rst = ""
\ No newline at end of file
diff --git a/BetterMD/elements/hgroup.py b/BetterMD/elements/hgroup.py
new file mode 100644
index 0000000..355a10e
--- /dev/null
+++ b/BetterMD/elements/hgroup.py
@@ -0,0 +1,6 @@
+from .symbol import Symbol
+
+class HGroup(Symbol):
+ md = ""
+ html = "hgroup"
+ rst = ""
\ No newline at end of file
diff --git a/BetterMD/elements/hr.py b/BetterMD/elements/hr.py
index 392d815..bdb2627 100644
--- a/BetterMD/elements/hr.py
+++ b/BetterMD/elements/hr.py
@@ -11,7 +11,9 @@ def to_rst(self, inner, symbol, parent):
return "----\n"
class Hr(Symbol):
+ prop_list = ["align", "color", "noshade", "size", "width"]
+
html = "hr"
md = MD()
rst = RST()
- nl = True
\ No newline at end of file
+ block = True
\ No newline at end of file
diff --git a/BetterMD/elements/html.py b/BetterMD/elements/html.py
new file mode 100644
index 0000000..cef62e5
--- /dev/null
+++ b/BetterMD/elements/html.py
@@ -0,0 +1,8 @@
+from .symbol import Symbol
+
+class HTML(Symbol):
+ prop_list = ["version", "xmlns"]
+
+ html = "html"
+ md = ""
+ rst = ""
\ No newline at end of file
diff --git a/BetterMD/elements/i.py b/BetterMD/elements/i.py
new file mode 100644
index 0000000..2a66f07
--- /dev/null
+++ b/BetterMD/elements/i.py
@@ -0,0 +1,16 @@
+from .symbol import Symbol
+from ..markdown import CustomMarkdown
+from ..rst import CustomRst
+
+class MD(CustomMarkdown):
+ def to_md(self, inner, symbol, parent):
+ return f"*{''.join([e.to_md() for e in inner])}*"
+
+class RST(CustomRst):
+ def to_rst(self, inner, symbol, parent):
+ return f"*{''.join([e.to_rst() for e in inner])}*"
+
+class I(Symbol):
+ html = "i"
+ md = MD()
+ rst = RST()
\ No newline at end of file
diff --git a/BetterMD/elements/iframe.py b/BetterMD/elements/iframe.py
new file mode 100644
index 0000000..50e3202
--- /dev/null
+++ b/BetterMD/elements/iframe.py
@@ -0,0 +1,12 @@
+from .symbol import Symbol
+
+class Iframe(Symbol):
+ prop_list = [
+ "allow", "allowfullscreen", "allowpaymentrequest", "browsingtopics", "credentialless", "csp",
+ "height", "loading", "name", "referrerpolicy", "sandbox",
+ "src", "srcdoc", "width"
+ ]
+
+ md = ""
+ html = "iframe"
+ rst = ""
\ No newline at end of file
diff --git a/BetterMD/elements/img.py b/BetterMD/elements/img.py
index dbc4031..b1484f6 100644
--- a/BetterMD/elements/img.py
+++ b/BetterMD/elements/img.py
@@ -6,18 +6,14 @@
class MD(CustomMarkdown):
def to_md(self, inner, symbol, parent):
alt = symbol.get_prop("alt", "")
- return f"})"
-
-class HTML(CustomHTML):
- def to_html(self, inner, symbol, parent):
- return f"
"
+ return f"})"
class RST(CustomRst):
def to_rst(self, inner, symbol, parent):
return f".. image:: {symbol.get_prop('src')}\n :alt: {symbol.get_prop("alt", "")}\n"
class Img(Symbol):
- props = ["src", "alt"]
+ prop_list = ["alt", "attributionsrc", "crossorigin", "decoding", "elementtiming", "fetchpriority", "height", "ismap", "loading", "referrerpolicy", "sizes", "src", "srcset", "width", "usemap", "align", "border", "hspace", "longdesc", "name", "vspace"]
md = MD()
- html = HTML()
+ html = "img"
rst = RST()
\ No newline at end of file
diff --git a/BetterMD/elements/input.py b/BetterMD/elements/input.py
index 557e90a..da4bea6 100644
--- a/BetterMD/elements/input.py
+++ b/BetterMD/elements/input.py
@@ -3,53 +3,58 @@
from ..markdown import CustomMarkdown
from ..rst import CustomRst
-class HTML(CustomHTML):
- def to_html(self, inner, symbol, parent):
- # Collect all input attributes
- attrs = []
- for prop in Input.props:
- value = symbol.get_prop(prop)
- if value:
- # Handle boolean attributes like 'required', 'disabled', etc.
- if isinstance(value, bool) and value:
- attrs.append(prop)
- else:
- attrs.append(f'{prop}="{value}"')
-
- attrs_str = " ".join(attrs)
- return f""
-
class MD(CustomMarkdown):
def to_md(self, inner, symbol, parent):
if symbol.get_prop("type") == "checkbox":
- return f"- [{'x' if symbol.get_prop('checked', '') else ''}] {inner.to_md()}"
+ return f"- [{'x' if symbol.get_prop('checked', '') else ' '}] {" ".join([elm.to_md() for elm in inner])}"
return symbol.to_html()
class RST(CustomRst):
def to_rst(self, inner, symbol, parent):
if symbol.get_prop("type") == "checkbox":
- return f"[ ] {inner.to_rst() if inner else ''}"
+ return f"[{'x' if symbol.get_prop('checked', '') else ' '}] {" ".join([elm.to_md() for elm in inner])}"
return "" # Most input types don't have RST equivalents
class Input(Symbol):
# Common input attributes
prop_list = [
- "type",
- "name",
- "value",
- "placeholder",
- "required",
- "disabled",
- "readonly",
- "min",
- "max",
- "pattern",
+ "accept",
+ "alt",
+ "autocapitalize",
"autocomplete",
"autofocus",
+ "capture",
"checked",
+ "dirname",
+ "disabled",
+ "form",
+ "formaction",
+ "formenctype",
+ "formmethod",
+ "formnovalidate",
+ "formtarget",
+ "height",
+ "list",
+ "max",
+ "maxlength",
+ "min",
+ "minlength",
"multiple",
- "step"
+ "name",
+ "pattern",
+ "placeholder",
+ "popovertarget",
+ "popovertargetaction",
+ "readonly",
+ "required",
+ "size",
+ "src",
+ "step",
+ "type",
+ "value",
+ "width",
]
- html = HTML()
+
+ html = "input"
md = MD()
rst = RST()
\ No newline at end of file
diff --git a/BetterMD/elements/ins.py b/BetterMD/elements/ins.py
new file mode 100644
index 0000000..c218c85
--- /dev/null
+++ b/BetterMD/elements/ins.py
@@ -0,0 +1,8 @@
+from .symbol import Symbol
+
+class Ins(Symbol):
+ prop_list = ["cite", "datetime"]
+
+ md = ""
+ html = "ins"
+ rst = ""
\ No newline at end of file
diff --git a/BetterMD/elements/kbd.py b/BetterMD/elements/kbd.py
new file mode 100644
index 0000000..f55cd10
--- /dev/null
+++ b/BetterMD/elements/kbd.py
@@ -0,0 +1,6 @@
+from .symbol import Symbol
+
+class Kbd(Symbol):
+ md = ""
+ html = "kbd"
+ rst = ""
\ No newline at end of file
diff --git a/BetterMD/elements/label.py b/BetterMD/elements/label.py
new file mode 100644
index 0000000..45010db
--- /dev/null
+++ b/BetterMD/elements/label.py
@@ -0,0 +1,8 @@
+from .symbol import Symbol
+
+class Label(Symbol):
+ prop_list = ["for"]
+
+ md = ""
+ html = "label"
+ rst = ""
\ No newline at end of file
diff --git a/BetterMD/elements/legend.py b/BetterMD/elements/legend.py
new file mode 100644
index 0000000..f9c3ef7
--- /dev/null
+++ b/BetterMD/elements/legend.py
@@ -0,0 +1,6 @@
+from .symbol import Symbol
+
+class Legend(Symbol):
+ md = ""
+ html = "legend"
+ rst = ""
\ No newline at end of file
diff --git a/BetterMD/elements/li.py b/BetterMD/elements/li.py
index 5c41cfc..e0aeb65 100644
--- a/BetterMD/elements/li.py
+++ b/BetterMD/elements/li.py
@@ -31,16 +31,21 @@ def to_rst(self, inner, symbol, parent) -> str:
class LI(Symbol):
+ prop_list = ["value", "type"]
+
html = "li"
md = MD()
rst = RST()
class OL(Symbol):
+ prop_list = ["reversed", "start", "type"]
html = "ol"
md = LMD()
rst = LRST()
class UL(Symbol):
+ prop_list = ["compact", "type"]
+
html = "ul"
md = LMD()
rst = LRST()
\ No newline at end of file
diff --git a/BetterMD/elements/link.py b/BetterMD/elements/link.py
new file mode 100644
index 0000000..adc523d
--- /dev/null
+++ b/BetterMD/elements/link.py
@@ -0,0 +1,8 @@
+from .symbol import Symbol
+
+class Link(Symbol):
+ prop_list = ["as", "blocking", "crossorigin", "disabled", "fetchpriority", "href", "hreflang", "imagesizes", "imagesrcset", "integrity", "media", "referrerpolicy", "rel", "sizes", "title", "type", "target", "charset", "rev"]
+
+ md = ""
+ html = "link"
+ rst = ""
\ No newline at end of file
diff --git a/BetterMD/elements/main.py b/BetterMD/elements/main.py
new file mode 100644
index 0000000..f92aad9
--- /dev/null
+++ b/BetterMD/elements/main.py
@@ -0,0 +1,6 @@
+from .symbol import Symbol
+
+class Main(Symbol):
+ md = ""
+ html = "main"
+ rst = ""
\ No newline at end of file
diff --git a/BetterMD/elements/map.py b/BetterMD/elements/map.py
new file mode 100644
index 0000000..966b453
--- /dev/null
+++ b/BetterMD/elements/map.py
@@ -0,0 +1,8 @@
+from .symbol import Symbol
+
+class Map(Symbol):
+ prop_list = ["name"]
+
+ md = ""
+ html = "map"
+ rst = ""
\ No newline at end of file
diff --git a/BetterMD/elements/mark.py b/BetterMD/elements/mark.py
new file mode 100644
index 0000000..a9280c1
--- /dev/null
+++ b/BetterMD/elements/mark.py
@@ -0,0 +1,6 @@
+from .symbol import Symbol
+
+class Mark(Symbol):
+ md = ""
+ html = "mark"
+ rst = ""
\ No newline at end of file
diff --git a/BetterMD/elements/marquee.py b/BetterMD/elements/marquee.py
new file mode 100644
index 0000000..df74f2d
--- /dev/null
+++ b/BetterMD/elements/marquee.py
@@ -0,0 +1,8 @@
+from .symbol import Symbol
+
+class Marquee(Symbol):
+ prop_list = ["behavior", "bgcolor", "direction", "height", "hspace", "loop", "scrollamount", "scrolldelay", "truespeed", "vspace", "width"]
+
+ md = ""
+ html = "marquee"
+ rst = ""
\ No newline at end of file
diff --git a/BetterMD/elements/menu.py b/BetterMD/elements/menu.py
new file mode 100644
index 0000000..81223e4
--- /dev/null
+++ b/BetterMD/elements/menu.py
@@ -0,0 +1,6 @@
+from .symbol import Symbol
+
+class Menu(Symbol):
+ md = ""
+ html = "menu"
+ rst = ""
\ No newline at end of file
diff --git a/BetterMD/elements/meta.py b/BetterMD/elements/meta.py
new file mode 100644
index 0000000..14d7377
--- /dev/null
+++ b/BetterMD/elements/meta.py
@@ -0,0 +1,8 @@
+from .symbol import Symbol
+
+class Meta(Symbol):
+ prop_list = ["charset", "content", "httpequiv", "media", "name"]
+
+ md = ""
+ html = "meta"
+ rst = ""
\ No newline at end of file
diff --git a/BetterMD/elements/meter.py b/BetterMD/elements/meter.py
new file mode 100644
index 0000000..26886fe
--- /dev/null
+++ b/BetterMD/elements/meter.py
@@ -0,0 +1,8 @@
+from .symbol import Symbol
+
+class Meter(Symbol):
+ prop_list = ["value", "min", "max", "low", "high", "optimum", "form"]
+
+ md = ""
+ html = "meter"
+ rst = ""
\ No newline at end of file
diff --git a/BetterMD/elements/nav.py b/BetterMD/elements/nav.py
new file mode 100644
index 0000000..7937844
--- /dev/null
+++ b/BetterMD/elements/nav.py
@@ -0,0 +1,6 @@
+from .symbol import Symbol
+
+class Nav(Symbol):
+ md = ""
+ html = "nav"
+ rst = ""
\ No newline at end of file
diff --git a/BetterMD/elements/no.py b/BetterMD/elements/no.py
new file mode 100644
index 0000000..a84608a
--- /dev/null
+++ b/BetterMD/elements/no.py
@@ -0,0 +1,21 @@
+from .symbol import Symbol
+
+class NoScript(Symbol):
+ md = ""
+ html = "noscript"
+ rst = ""
+
+class NoFrames(Symbol):
+ md = ""
+ html = "noframes"
+ rst = ""
+
+class NoBr(Symbol):
+ md = ""
+ html = "nobr"
+ rst = ""
+
+class NoEmbed(Symbol):
+ md = ""
+ html = "noembed"
+ rst = ""
\ No newline at end of file
diff --git a/BetterMD/elements/object.py b/BetterMD/elements/object.py
new file mode 100644
index 0000000..8f34a9b
--- /dev/null
+++ b/BetterMD/elements/object.py
@@ -0,0 +1,8 @@
+from .symbol import Symbol
+
+class Object(Symbol):
+ prop_list = ["archive", "border", "classid", "codebase", "codetype", "data", "declare", "form", "height", "name", "standby", "type", "usemap", "width"]
+
+ md = ""
+ html = "object"
+ rst = ""
\ No newline at end of file
diff --git a/BetterMD/elements/output.py b/BetterMD/elements/output.py
new file mode 100644
index 0000000..c72f34a
--- /dev/null
+++ b/BetterMD/elements/output.py
@@ -0,0 +1,8 @@
+from .symbol import Symbol
+
+class Output(Symbol):
+ prop_list = ["for", "form", "name"]
+
+ md = ""
+ html = "output"
+ rst = ""
\ No newline at end of file
diff --git a/BetterMD/elements/p.py b/BetterMD/elements/p.py
index b5df935..24e6604 100644
--- a/BetterMD/elements/p.py
+++ b/BetterMD/elements/p.py
@@ -4,4 +4,9 @@ class P(Symbol):
html = "p"
md = ""
rst = "\n\n"
- nl = True
\ No newline at end of file
+ block = True
+
+class Pre(Symbol):
+ html = "pre"
+ md = ""
+ rst = ""
diff --git a/BetterMD/elements/param.py b/BetterMD/elements/param.py
new file mode 100644
index 0000000..e2fbe63
--- /dev/null
+++ b/BetterMD/elements/param.py
@@ -0,0 +1,8 @@
+from .symbol import Symbol
+
+class Param(Symbol):
+ prop_list = ["name", "value", "type", "valuetype"]
+
+ md = ""
+ html = "param"
+ rst = ""
\ No newline at end of file
diff --git a/BetterMD/elements/picture.py b/BetterMD/elements/picture.py
new file mode 100644
index 0000000..f4091b3
--- /dev/null
+++ b/BetterMD/elements/picture.py
@@ -0,0 +1,6 @@
+from .symbol import Symbol
+
+class Picture(Symbol):
+ md = ""
+ html = "picture"
+ rst = ""
\ No newline at end of file
diff --git a/BetterMD/elements/plaintext.py b/BetterMD/elements/plaintext.py
new file mode 100644
index 0000000..8a126ea
--- /dev/null
+++ b/BetterMD/elements/plaintext.py
@@ -0,0 +1,6 @@
+from .symbol import Symbol
+
+class Plaintext(Symbol):
+ md = ""
+ html = "plaintext"
+ rst = ""
\ No newline at end of file
diff --git a/BetterMD/elements/progress.py b/BetterMD/elements/progress.py
new file mode 100644
index 0000000..c64785e
--- /dev/null
+++ b/BetterMD/elements/progress.py
@@ -0,0 +1,8 @@
+from .symbol import Symbol
+
+class Progress(Symbol):
+ prop_list = ["max", "value"]
+
+ md = ""
+ html = "progress"
+ rst = ""
\ No newline at end of file
diff --git a/BetterMD/elements/q.py b/BetterMD/elements/q.py
new file mode 100644
index 0000000..8fa9a56
--- /dev/null
+++ b/BetterMD/elements/q.py
@@ -0,0 +1,8 @@
+from .symbol import Symbol
+
+class Q(Symbol):
+ prop_list = ["cite"]
+
+ md = ""
+ html = "q"
+ rst = ""
\ No newline at end of file
diff --git a/BetterMD/elements/ruby.py b/BetterMD/elements/ruby.py
new file mode 100644
index 0000000..16aa8f6
--- /dev/null
+++ b/BetterMD/elements/ruby.py
@@ -0,0 +1,26 @@
+from .symbol import Symbol
+
+class RB(Symbol):
+ md = ""
+ html = "rb"
+ rst = ""
+
+class RP(Symbol):
+ md = ""
+ html = "rp"
+ rst = ""
+
+class RT(Symbol):
+ md = ""
+ html = "rt"
+ rst = ""
+
+class RTC(Symbol):
+ md = ""
+ html = "rtc"
+ rst = ""
+
+class Ruby(Symbol):
+ md = ""
+ html = "ruby"
+ rst = ""
\ No newline at end of file
diff --git a/BetterMD/elements/s.py b/BetterMD/elements/s.py
new file mode 100644
index 0000000..af0e1ae
--- /dev/null
+++ b/BetterMD/elements/s.py
@@ -0,0 +1,6 @@
+from .symbol import Symbol
+
+class S(Symbol):
+ md = ""
+ html = "s"
+ rst = ""
\ No newline at end of file
diff --git a/BetterMD/elements/samp.py b/BetterMD/elements/samp.py
new file mode 100644
index 0000000..c6a4958
--- /dev/null
+++ b/BetterMD/elements/samp.py
@@ -0,0 +1,6 @@
+from .symbol import Symbol
+
+class Samp(Symbol):
+ md = ""
+ html = "samp"
+ rst = ""
\ No newline at end of file
diff --git a/BetterMD/elements/script.py b/BetterMD/elements/script.py
new file mode 100644
index 0000000..23ee323
--- /dev/null
+++ b/BetterMD/elements/script.py
@@ -0,0 +1,8 @@
+from .symbol import Symbol
+
+class Script(Symbol):
+ prop_list = ["async", "attributionsrc", "blocking", "crossorigin", "defer", "fetchpriority", "integrity", "nomodule", "none", "referrerpolicy", "src", "type", "charset", "language"]
+
+ md = ""
+ html = "script"
+ rst = ""
\ No newline at end of file
diff --git a/BetterMD/elements/search.py b/BetterMD/elements/search.py
new file mode 100644
index 0000000..70de6df
--- /dev/null
+++ b/BetterMD/elements/search.py
@@ -0,0 +1,6 @@
+from .symbol import Symbol
+
+class Search(Symbol):
+ md = ""
+ html = "search"
+ rst = ""
\ No newline at end of file
diff --git a/BetterMD/elements/section.py b/BetterMD/elements/section.py
new file mode 100644
index 0000000..abf2ab2
--- /dev/null
+++ b/BetterMD/elements/section.py
@@ -0,0 +1,6 @@
+from .symbol import Symbol
+
+class Section(Symbol):
+ md = ""
+ html = "section"
+ rst = ""
\ No newline at end of file
diff --git a/BetterMD/elements/select.py b/BetterMD/elements/select.py
new file mode 100644
index 0000000..52c59f5
--- /dev/null
+++ b/BetterMD/elements/select.py
@@ -0,0 +1,22 @@
+from . import Symbol
+
+class Select(Symbol):
+ prop_list = ["autocomplete", "autofocus", "disabled", "form", "multiple", "name", "required", "size"]
+
+ md = ""
+ html = "select"
+ rst = ""
+
+class Option(Symbol):
+ prop_list = ["disabled", "label", "selected", "value"]
+
+ md = ""
+ html = "option"
+ rst = ""
+
+class Optgroup(Symbol):
+ prop_list = ["disabled", "label"]
+
+ md = ""
+ html = "optgroup"
+ rst = ""
\ No newline at end of file
diff --git a/BetterMD/elements/slot.py b/BetterMD/elements/slot.py
new file mode 100644
index 0000000..00346d5
--- /dev/null
+++ b/BetterMD/elements/slot.py
@@ -0,0 +1,8 @@
+from .symbol import Symbol
+
+class Slot(Symbol):
+ prop_list = ["name"]
+
+ md = ""
+ html = "slot"
+ rst = ""
\ No newline at end of file
diff --git a/BetterMD/elements/small.py b/BetterMD/elements/small.py
new file mode 100644
index 0000000..cbf6fda
--- /dev/null
+++ b/BetterMD/elements/small.py
@@ -0,0 +1,6 @@
+from .symbol import Symbol
+
+class Small(Symbol):
+ md = ""
+ html = "small"
+ rst = ""
\ No newline at end of file
diff --git a/BetterMD/elements/source.py b/BetterMD/elements/source.py
new file mode 100644
index 0000000..602e4c3
--- /dev/null
+++ b/BetterMD/elements/source.py
@@ -0,0 +1,8 @@
+from .symbol import Symbol
+
+class Source(Symbol):
+ prop_list = ["type", "src", "srcset", "sizes", "media", "width"]
+
+ md = ""
+ html = "source"
+ rst = ""
\ No newline at end of file
diff --git a/BetterMD/elements/strike.py b/BetterMD/elements/strike.py
new file mode 100644
index 0000000..3623080
--- /dev/null
+++ b/BetterMD/elements/strike.py
@@ -0,0 +1,6 @@
+from .symbol import Symbol
+
+class Strike(Symbol):
+ md = ""
+ html = "strike"
+ rst = ""
\ No newline at end of file
diff --git a/BetterMD/elements/style.py b/BetterMD/elements/style.py
new file mode 100644
index 0000000..3423155
--- /dev/null
+++ b/BetterMD/elements/style.py
@@ -0,0 +1,84 @@
+from .symbol import Symbol
+from ..html import CustomHTML
+from ..typing import ATTR_TYPES
+
+import typing as t
+
+StyleValue = t.Union[str, int, float, tuple[t.Union[str, int, float], ...]]
+StyleDict = dict[str, t.Union[StyleValue, 'StyleDict']]
+
+class HTML(CustomHTML['Style']):
+ def verify(self, text) -> bool:
+ return text.lower() == "style"
+
+ def _format_value(self, value: 'StyleValue') -> 'str':
+ """Format a style value for CSS output"""
+ if isinstance(value, tuple):
+ return " ".join(str(v) for v in value)
+ return str(value)
+
+ def _process_styles(self, selector: 'str', styles: 'StyleDict') -> 'list[str]':
+ """Process styles recursively and return CSS rules"""
+ result = []
+ properties = {}
+ nested = {}
+
+ # Separate properties from nested selectors
+ for key, value in styles.items():
+ if isinstance(value, dict):
+ nested[key] = value
+ else:
+ properties[key] = value
+
+ # Add properties for current selector if any
+ if properties:
+ result.append(f"{selector} {{")
+ for prop, value in properties.items():
+ formatted_value = self._format_value(value)
+ result.append(f" {prop}: {formatted_value};")
+ result.append("}")
+
+ # Process nested selectors
+ for key, value in nested.items():
+ if key.startswith(':'): # Pseudo-class
+ nested_selector = f"{selector}{key}"
+ elif key.startswith('#'): # ID
+ nested_selector = f"{selector} {key}"
+ elif key.startswith('.'): # Class
+ nested_selector = f"{selector} {key}"
+ else: # Element or custom
+ nested_selector = f"{selector} {key}"
+
+ result.extend(self._process_styles(nested_selector, value))
+
+ return result
+
+ def to_html(self, inner, symbol, parent):
+ style_str = []
+
+ for selector, rules in symbol.style.items():
+ style_str.extend(self._process_styles(selector, rules))
+
+ return f""
+
+
+class Style(Symbol):
+ def __init__(self, styles:'dict[str, ATTR_TYPES]'=None, classes:'list[str]'=None, inner:'list[Symbol]'=None, *, style: t.Optional[StyleDict] = None, raw: str = "",**props):
+ """
+ Styles with intuitive nested structure
+
+ Args:
+ style: Dictionary of style rules with nested selectors
+ raw: Original raw CSS text
+ inner: Child symbols
+ **props: Additional properties
+ """
+ super().__init__(styles, classes, inner, **props)
+ self.style: 'StyleDict' = style or {}
+ self.raw: 'str' = raw
+
+ prop_list = ["blocking", "media", "nonce", "title", "type"]
+
+ html = HTML()
+ md = ""
+ rst = ""
\ No newline at end of file
diff --git a/BetterMD/elements/sub.py b/BetterMD/elements/sub.py
new file mode 100644
index 0000000..0a46075
--- /dev/null
+++ b/BetterMD/elements/sub.py
@@ -0,0 +1,6 @@
+from .symbol import Symbol
+
+class Sub(Symbol):
+ html = "sub"
+ md = ""
+ rst = ""
\ No newline at end of file
diff --git a/BetterMD/elements/summary.py b/BetterMD/elements/summary.py
new file mode 100644
index 0000000..f9d4eba
--- /dev/null
+++ b/BetterMD/elements/summary.py
@@ -0,0 +1,6 @@
+from .symbol import Symbol
+
+class Summary(Symbol):
+ html = "summary"
+ md = ""
+ rst = ""
\ No newline at end of file
diff --git a/BetterMD/elements/sup.py b/BetterMD/elements/sup.py
new file mode 100644
index 0000000..8a522ae
--- /dev/null
+++ b/BetterMD/elements/sup.py
@@ -0,0 +1,6 @@
+from .symbol import Symbol
+
+class Sup(Symbol):
+ html = "sup"
+ md = ""
+ rst = ""
\ No newline at end of file
diff --git a/BetterMD/elements/svg.py b/BetterMD/elements/svg.py
new file mode 100644
index 0000000..df4256e
--- /dev/null
+++ b/BetterMD/elements/svg.py
@@ -0,0 +1,399 @@
+from .symbol import Symbol
+
+# Check prop lists before use
+# MDN Docs: https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Element/svg
+
+# Animation elements
+class Animate(Symbol):
+ html = "animate"
+ md = ""
+ rst = ""
+
+class AnimateMotion(Symbol):
+ html = "animateMotion"
+ md = ""
+ rst = ""
+
+class AnimateTransform(Symbol):
+ html = "animateTransform"
+ md = ""
+ rst = ""
+
+# Shape elements
+class Circle(Symbol):
+ prop_list = ["cx", "cy", "r"]
+ html = "circle"
+ md = ""
+ rst = ""
+
+class ClipPath(Symbol):
+ html = "clipPath"
+ md = ""
+ rst = ""
+
+class Cursor(Symbol):
+ html = "cursor"
+ md = ""
+ rst = ""
+
+class Defs(Symbol):
+ html = "defs"
+ md = ""
+ rst = ""
+
+class Desc(Symbol):
+ html = "desc"
+ md = ""
+ rst = ""
+
+class Discard(Symbol):
+ html = "discard"
+ md = ""
+ rst = ""
+
+class Ellipse(Symbol):
+ prop_list = ["cx", "cy", "rx", "ry"]
+ html = "ellipse"
+ md = ""
+ rst = ""
+
+# Filter elements
+class FeBlend(Symbol):
+ html = "feBlend"
+ md = ""
+ rst = ""
+
+class FeColorMatrix(Symbol):
+ html = "feColorMatrix"
+ md = ""
+ rst = ""
+
+class FeComponentTransfer(Symbol):
+ html = "feComponentTransfer"
+ md = ""
+ rst = ""
+
+class FeComposite(Symbol):
+ html = "feComposite"
+ md = ""
+ rst = ""
+
+class FeConvolveMatrix(Symbol):
+ html = "feConvolveMatrix"
+ md = ""
+ rst = ""
+
+class FeDiffuseLighting(Symbol):
+ html = "feDiffuseLighting"
+ md = ""
+ rst = ""
+
+class FeDisplacementMap(Symbol):
+ html = "feDisplacementMap"
+ md = ""
+ rst = ""
+
+class FeDistantLight(Symbol):
+ html = "feDistantLight"
+ md = ""
+ rst = ""
+
+class FeDropShadow(Symbol):
+ html = "feDropShadow"
+ md = ""
+ rst = ""
+
+class FeFlood(Symbol):
+ html = "feFlood"
+ md = ""
+ rst = ""
+
+class FeFuncA(Symbol):
+ html = "feFuncA"
+ md = ""
+ rst = ""
+
+class FeFuncB(Symbol):
+ html = "feFuncB"
+ md = ""
+ rst = ""
+
+class FeFuncG(Symbol):
+ html = "feFuncG"
+ md = ""
+ rst = ""
+
+class FeFuncR(Symbol):
+ html = "feFuncR"
+ md = ""
+ rst = ""
+
+class FeGaussianBlur(Symbol):
+ html = "feGaussianBlur"
+ md = ""
+ rst = ""
+
+class FeImage(Symbol):
+ html = "feImage"
+ md = ""
+ rst = ""
+
+class FeMerge(Symbol):
+ html = "feMerge"
+ md = ""
+ rst = ""
+
+class FeMergeNode(Symbol):
+ html = "feMergeNode"
+ md = ""
+ rst = ""
+
+class FeMorphology(Symbol):
+ html = "feMorphology"
+ md = ""
+ rst = ""
+
+class FeOffset(Symbol):
+ html = "feOffset"
+ md = ""
+ rst = ""
+
+class FePointLight(Symbol):
+ html = "fePointLight"
+ md = ""
+ rst = ""
+
+class FeSpecularLighting(Symbol):
+ html = "feSpecularLighting"
+ md = ""
+ rst = ""
+
+class FeSpotLight(Symbol):
+ html = "feSpotLight"
+ md = ""
+ rst = ""
+
+class FeTile(Symbol):
+ html = "feTile"
+ md = ""
+ rst = ""
+
+class FeTurbulence(Symbol):
+ html = "feTurbulence"
+ md = ""
+ rst = ""
+
+class Filter(Symbol):
+ html = "filter"
+ md = ""
+ rst = ""
+
+# Font elements (deprecated but included)
+class FontFaceFormat(Symbol):
+ html = "font-face-format"
+ md = ""
+ rst = ""
+
+class FontFaceName(Symbol):
+ html = "font-face-name"
+ md = ""
+ rst = ""
+
+class FontFaceSrc(Symbol):
+ html = "font-face-src"
+ md = ""
+ rst = ""
+
+class FontFaceUri(Symbol):
+ html = "font-face-uri"
+ md = ""
+ rst = ""
+
+class FontFace(Symbol):
+ html = "font-face"
+ md = ""
+ rst = ""
+
+class Font(Symbol):
+ html = "font"
+ md = ""
+ rst = ""
+
+# Other SVG elements
+class ForeignObject(Symbol):
+ html = "foreignObject"
+ md = ""
+ rst = ""
+
+class G(Symbol):
+ html = "g"
+ md = ""
+ rst = ""
+
+class Glyph(Symbol):
+ html = "glyph"
+ md = ""
+ rst = ""
+
+class GlyphRef(Symbol):
+ html = "glyphRef"
+ md = ""
+ rst = ""
+
+class HKern(Symbol):
+ html = "hkern"
+ md = ""
+ rst = ""
+
+class Image(Symbol):
+ prop_list = ["href", "x", "y", "width", "height"]
+ html = "image"
+ md = ""
+ rst = ""
+
+class Line(Symbol):
+ prop_list = ["x1", "y1", "x2", "y2"]
+ html = "line"
+ md = ""
+ rst = ""
+
+class LinearGradient(Symbol):
+ html = "linearGradient"
+ md = ""
+ rst = ""
+
+class Marker(Symbol):
+ html = "marker"
+ md = ""
+ rst = ""
+
+class Mask(Symbol):
+ html = "mask"
+ md = ""
+ rst = ""
+
+class Metadata(Symbol):
+ html = "metadata"
+ md = ""
+ rst = ""
+
+class MissingGlyph(Symbol):
+ html = "missing-glyph"
+ md = ""
+ rst = ""
+
+class MPath(Symbol):
+ html = "mpath"
+ md = ""
+ rst = ""
+
+class Path(Symbol):
+ prop_list = ["d"]
+ html = "path"
+ md = ""
+ rst = ""
+
+class Pattern(Symbol):
+ html = "pattern"
+ md = ""
+ rst = ""
+
+class Polygon(Symbol):
+ prop_list = ["points"]
+ html = "polygon"
+ md = ""
+ rst = ""
+
+class Polyline(Symbol):
+ prop_list = ["points"]
+ html = "polyline"
+ md = ""
+ rst = ""
+
+class RadialGradient(Symbol):
+ html = "radialGradient"
+ md = ""
+ rst = ""
+
+class Rect(Symbol):
+ prop_list = ["x", "y", "width", "height", "rx", "ry"]
+ html = "rect"
+ md = ""
+ rst = ""
+
+class SVGScript(Symbol):
+ html = "script"
+ md = ""
+ rst = ""
+
+class Set(Symbol):
+ html = "set"
+ md = ""
+ rst = ""
+
+class Stop(Symbol):
+ html = "stop"
+ md = ""
+ rst = ""
+
+class Style(Symbol):
+ html = "style"
+ md = ""
+ rst = ""
+
+class Svg(Symbol):
+ prop_list = ["width", "height", "viewBox"]
+ html = "svg"
+ md = ""
+ rst = ""
+
+class Switch(Symbol):
+ html = "switch"
+ md = ""
+ rst = ""
+
+class SVGSymbol(Symbol):
+ html = "symbol"
+ md = ""
+ rst = ""
+
+class SVGText(Symbol):
+ html = "text"
+ md = ""
+ rst = ""
+
+class TextPath(Symbol):
+ html = "textPath"
+ md = ""
+ rst = ""
+
+class Title(Symbol):
+ html = "title"
+ md = ""
+ rst = ""
+
+class TRef(Symbol):
+ html = "tref"
+ md = ""
+ rst = ""
+
+class TSpan(Symbol):
+ html = "tspan"
+ md = ""
+ rst = ""
+
+class Use(Symbol):
+ prop_list = ["href", "x", "y", "width", "height"]
+ html = "use"
+ md = ""
+ rst = ""
+
+class View(Symbol):
+ html = "view"
+ md = ""
+ rst = ""
+
+class VKern(Symbol):
+ html = "vkern"
+ md = ""
+ rst = ""
\ No newline at end of file
diff --git a/BetterMD/elements/symbol.py b/BetterMD/elements/symbol.py
index 2dfbb3e..93035ad 100644
--- a/BetterMD/elements/symbol.py
+++ b/BetterMD/elements/symbol.py
@@ -3,37 +3,57 @@
from ..markdown import CustomMarkdown
from ..html import CustomHTML
from ..rst import CustomRst
+from ..parse import HTMLParser, MDParser, ELEMENT, TEXT, Collection
+from ..utils import List, set_recursion_limit
+from ..typing import ATTR_TYPES
+
+set_recursion_limit(10000)
class Symbol:
- styles: 'dict[str, str]' = {}
- classes: 'list[str]' = []
html: 't.Union[str, CustomHTML]' = ""
- props: 'dict[str, str]' = {}
prop_list: 'list[str]' = []
- vars:'dict[str,str]' = {}
- children:'list[Symbol]' = []
md: 't.Union[str, CustomMarkdown]' = ""
rst: 't.Union[str, CustomRst]' = ""
- parent:'Symbol' = None
- prepared:'bool' = False
nl:'bool' = False
+ block: 'bool' = False
+ self_closing: 'bool' = False
- html_written_props = ""
+ collection = Collection()
+ html_parser = HTMLParser()
+ md_parser = MDParser()
+
+ def __init_subclass__(cls, **kwargs) -> None:
+ cls.collection.add_symbols(cls)
+ super().__init_subclass__(**kwargs)
+
+ def __init__(self, styles:'dict[str,str]'=None, classes:'list[str]'=None, inner:'list[Symbol]'=None, **props:'ATTR_TYPES'):
+ self.parent:'Symbol' = None
+ self.prepared:'bool' = False
+ self.html_written_props = ""
+
+ if styles is None:
+ styles = {}
+ if classes is None:
+ classes = []
+ if inner is None:
+ inner = []
+
+ self.styles: 'dict[str, str]' = styles
+ self.classes: 'list[str]' = classes
+ self.children:'list[Symbol]' = list(inner) or []
+ self.props: 'dict[str, ATTR_TYPES]' = props
+
+ def copy(self, styles:'dict[str,str]'=None, classes:'list[str]'=None, inner:'list[Symbol]'=None):
+ if inner is None:
+ inner = []
+ if styles is None:
+ styles = {}
+ if classes is None:
+ classes = []
- def __init__(self, styles:'dict[str,str]'={}, classes:'list[str]'=[], dom:'bool'=True, inner:'list[Symbol]'=[], **props):
- self.styles = styles
- self.classes = classes
- self.children = list(inner) or []
- self.props = props
- self.dom = dom
-
- def copy(self, styles:'dict[str,str]'={}, classes:'list[str]'=[], inner:'list[Symbol]'=None):
- if inner == None:
- inner = [Symbol()]
styles.update(self.styles)
return Symbol(styles, classes, inner = inner)
-
-
+
def set_parent(self, parent:'Symbol'):
self.parent = parent
self.parent.add_child(self)
@@ -47,51 +67,136 @@ def add_child(self, symbol:'Symbol'):
def remove_child(self, symbol:'Symbol'):
self.children.remove(symbol)
+
+ def extend_children(self, symbols:'list[Symbol]'):
+ self.children.extend(symbols)
def has_child(self, child:'type[Symbol]'):
for e in self.children:
if isinstance(e, child):
return e
-
return False
- def prepare(self, parent:'Symbol'):
+ def prepare(self, parent:'Symbol', *args, **kwargs):
self.prepared = True
self.parent = parent
for symbol in self.children:
- symbol.prepare(self)
-
+ symbol.prepare(self, *args, **kwargs)
+
return self
def replace_child(self, old:'Symbol', new:'Symbol'):
i = self.children.index(old)
- self.children.remove(old)
+ self.children[i] = new
- self.children[i-1] = new
-
+ def handle_props(self, p):
+ props = {**({"class": self.classes} if self.classes else {}), **({"style": self.styles} if self.styles else {}), **self.props}
+ prop_list = []
+ for k, v in props.items():
+ if isinstance(v, bool) or v == "":
+ prop_list.append(f"{k}" if v else "")
+ elif isinstance(v, (int, float, str)):
+ prop_list.append(f'{k}="{v}"')
+ elif isinstance(v, list):
+ prop_list.append(f'{k}="{" ".join(v)}"')
+ elif isinstance(v, dict):
+ prop_list.append(f'{k}="{"; ".join([f"{k}:{v}" for k,v in v.items()])}"')
+ else:
+ raise TypeError(f"Unsupported type for prop {k}: {type(v)}")
+ return (" " + " ".join(filter(None, prop_list))) if prop_list else ""
- def to_html(self) -> 'str':
+ def to_html(self, indent=0) -> 'str':
if isinstance(self.html, CustomHTML):
return self.html.to_html(self.children, self, self.parent)
- inner_HTML = "\n".join([e.to_html() for e in self.children])
- return f"<{self.html} class={' '.join(self.classes) or '""'} style={' '.join([f'{k}:{v}' for k,v in self.styles.items()]) or '""'} {' '.join([f'{k}={v}'for k,v in self.props.items()])}>{inner_HTML}{self.html}>"
-
+ inner_HTML = "\n".join([
+ e.to_html(0) if not (len(self.children) == 1 and isinstance(e.html, str) and e.html == "text")
+ else e.to_html(0) for e in self.children
+ ])
+
+ if inner_HTML or not self.self_closing:
+ return f"<{self.html}{self.handle_props(False)}>{inner_HTML}{self.html}>"
+ else:
+ return f"<{self.html}{self.handle_props(False)} />"
+
def to_md(self) -> 'str':
if isinstance(self.md, CustomMarkdown):
return self.md.to_md(self.children, self, self.parent)
+
+ inner_md = ""
- inner_md = " ".join([e.to_md() for e in self.children])
- return f"{self.md} {inner_md}" + ("\n" if self.nl else "")
-
+ for e in self.children:
+ if e.block:
+ inner_md += f"\n{e.to_md()}\n"
+ elif e.nl:
+ inner_md += f"{e.to_md()}\n"
+ else:
+ inner_md += f"{e.to_md()}"
+
+ return f"{self.md}{inner_md}"
+
def to_rst(self) -> 'str':
if isinstance(self.rst, CustomRst):
return self.rst.to_rst(self.children, self, self.parent)
-
+
inner_rst = " ".join([e.to_rst() for e in self.children])
return f"{self.rst}{inner_rst}{self.rst}\n"
-
- def get_prop(self, prop, default="") -> 'str':
+
+ @classmethod
+ def from_html(cls, text:'str') -> 'List[Symbol]':
+ parsed = cls.html_parser.parse(text)
+ return List([cls.collection.find_symbol(elm['name'], raise_errors=True).parse(elm) for elm in parsed])
+
+ @classmethod
+ def from_md(cls, text: str) -> 'List[Symbol]':
+ parsed = cls.md_parser.parse(text)
+ return List([cls.collection.find_symbol(elm['name'] , raise_errors=True).parse(elm) for elm in parsed])
+
+ @classmethod
+ def parse(cls, text:'ELEMENT|TEXT') -> 'Symbol':
+ def handle_element(element:'ELEMENT|TEXT'):
+ if element['type'] == 'text':
+ text = cls.collection.find_symbol("text", raise_errors=True)
+ assert text is not None, "`collection.find_symbol` is broken"
+ return text(element['content'])
+
+ symbol_cls = cls.collection.find_symbol(element['name'], raise_errors=True)
+ assert symbol_cls is not None, "`collection.find_symbol` is broken"
+
+ return symbol_cls.parse(element)
+
+ if text["type"] == "text":
+ return cls.collection.find_symbol("text", raise_errors=True)(text["content"])
+
+ # Extract attributes directly from the attributes dictionary
+ attributes = text["attributes"]
+
+ # Handle class attribute separately if it exists
+ classes = []
+ if "class" in attributes:
+ classes = attributes["class"].split() if isinstance(attributes["class"], str) else attributes["class"]
+ del attributes["class"]
+
+ # Handle style attribute separately if it exists
+ styles = {}
+ if "style" in attributes:
+ style_str = attributes["style"]
+ if isinstance(style_str, str):
+ styles = dict(item.split(":") for item in style_str.split(";") if ":" in item)
+ elif isinstance(style_str, dict):
+ styles = style_str
+ del attributes["style"]
+
+ inner=[handle_element(elm) for elm in text["children"]]
+
+ return cls(
+ styles=styles,
+ classes=classes,
+ inner=inner,
+ **attributes
+ )
+
+ def get_prop(self, prop, default=""):
return self.props.get(prop, default)
def set_prop(self, prop, value):
@@ -101,3 +206,8 @@ def __contains__(self, item):
if callable(item):
return any(isinstance(e, item) for e in self.children)
return item in self.children
+
+ def __str__(self):
+ return f"<{self.html}{self.handle_props()} />"
+
+ __repr__ = __str__
\ No newline at end of file
diff --git a/BetterMD/elements/table.py b/BetterMD/elements/table.py
index 3d7cbbe..e08408f 100644
--- a/BetterMD/elements/table.py
+++ b/BetterMD/elements/table.py
@@ -1,113 +1,487 @@
from .symbol import Symbol
+from ..utils import List
from ..markdown import CustomMarkdown
from ..rst import CustomRst
-from .h import H1, H2, H3, H4, H5, H6
from .text import Text
+import logging
+import typing as t
-class TableMD(CustomMarkdown):
- def to_md(self, inner, symbol, parent):
- return "\n".join([e.to_md() for e in inner]) + "\n"
+if t.TYPE_CHECKING:
+ # Wont be imported at runtime
+ import pandas as pd # If not installed, will not affect anything at runtime
+
+
+logger = logging.getLogger("BetterMD")
+T = t.TypeVar("T")
class TrMD(CustomMarkdown['Tr']):
- def to_md(self, inner, symbol, parent):
- if symbol.is_header:
- cells = [e.to_md() for e in inner]
- separator = ["---" for _ in cells]
- return f"|{'|'.join(cells)}|\n|{'|'.join(separator)}|"
- return f"|{'|'.join([e.to_md() for e in inner])}|"
-
-class TdMD(CustomMarkdown):
- def to_md(self, inner, symbol, parent):
- return " ".join([e.to_md() for e in inner])
-
-class TableRST(CustomRst):
- def to_rst(self, inner, symbol, parent):
- if not inner:
- return ""
-
- # Get all rows and their cells
- rows = [[cell.to_rst() for cell in row.children] for row in inner]
+ def to_md(self, inner, symbol, parent, pretty=True, **kwargs):
+ logger.debug("Converting Tr element to Markdown")
+ contents = "\n".join([e.to_md() for e in inner])
+ split_content = contents.splitlines()
+ logger.debug(f"Split content: {split_content}")
+ ret = f"| {" | ".join(split_content)} |"
+ return ret
+
+
+class THeadMD(CustomMarkdown['THead']):
+ def to_md(self, inner, symbol, parent, pretty=True, **kwargs):
+ md = []
+ for child in symbol.head.children:
+ e = child.to_md()
+
+ md.append({"len":len(e), "style":child.styles.get("text-align", "justify")})
+
+ def parse_md(data: 'dict') -> 'str':
+ start = " :" if data["style"] in ["left", "center"] else " "
+ middle = "-"*(data["len"]-2) if data["style"] == "center" else "-"*(data["len"]-1) if data["style"] in ["left", "right"] else "-"*(data["len"])
+ end = ": " if data["style"] in ["right", "center"] else " "
+
+ return f"{start}{middle}{end}"
+
+ return f"{inner[0].to_md()}\n|{"|".join([parse_md(item) for item in md])}|"
- # Calculate max width for each column
- num_cols = max(len(row) for row in rows)
- col_widths = [0] * num_cols
- for row in rows:
- for i, cell in enumerate(row):
- col_widths[i] = max(col_widths[i], len(cell))
+class TBodyMD(CustomMarkdown['TBody']):
+ def to_md(self, inner, symbol, parent, pretty=True, **kwargs):
+ content = [e.to_md() for e in inner if isinstance(e, Tr)]
+ logger.debug(f"TBody conent: {content}")
+ return "\n".join(content)
+
+class TdMD(CustomMarkdown['Td']):
+ def to_md(self, inner, symbol, parent, pretty=True, **kwargs):
+ if not pretty:
+ return " ".join([e.to_md() for e in inner])
+
+ length = len(max(symbol.table.cols[symbol.header], key=len).data)
+ logger.debug(f"Td length: {len(symbol)}")
+ logger.debug(f"Column length: {length}")
+ return " ".join([e.to_md() for e in inner]).center(length)
+
+class ThMD(CustomMarkdown['Th']):
+ def to_md(self, inner, symbol, parent, pretty=True, **kwargs):
+ if not pretty:
+ return " ".join([e.to_md() for e in inner])
- # Create the table string
- table = []
+ width = len(max(symbol.table.cols[symbol.header], key=len).data)
+
- # Create top border
- border = "+" + "+".join("-" * (width + 2) for width in col_widths) + "+"
- table.append(border)
+ if symbol.data == "":
+ return "".center(width)
- # Add rows with proper formatting
- for i, row in enumerate(inner):
- cells = [cell.to_rst() for cell in row.children]
- row_str = "| " + " | ".join(cell.ljust(width) for cell, width in zip(cells, col_widths)) + " |"
- table.append(row_str)
-
- # Add header separator or row separator
- if row.is_header:
- separator = "+" + "+".join("=" * (width + 2) for width in col_widths) + "+"
- else:
- separator = "+" + "+".join("-" * (width + 2) for width in col_widths) + "+"
- table.append(separator)
-
- return "\n".join(table)
+ return f"**{" ".join([e.to_md() for e in inner]).center(width)}**"
+
+class TableMD(CustomMarkdown['Table']):
+ def to_md(self, inner, symbol, parent, pretty=True, **kwargs):
+ logger.debug("Converting Table element to Markdown")
+ head = symbol.head.to_md() if symbol.head else None
+ body = symbol.body.to_md()
-class ThRST(CustomRst):
- def to_rst(self, inner, symbol, parent):
+ logger.debug(f"Table conversion complete. Has header: {head is not None}")
+ return f"{f"{head}\n" if head else ""}{body}"
+
+
+class TableRST(CustomRst['Table']):
+ def to_rst(self, inner, symbol, parent, **kwargs):
+ logger.debug("Converting Table element to RST")
+ head = symbol.head.to_rst() if symbol.head else None
+ body = symbol.body.to_rst()
+
+ return f"{f"{head}\n" if head else ""}{body}"
+
+class THeadRST(CustomRst['THead']):
+ def to_rst(self, inner, symbol, parent, **kwargs):
+ logger.debug("Converting THead element to RST")
+ logger.debug(f"THead has {len(inner)} children: {[e.to_rst() for e in inner]}")
+ top = [len(max(symbol.table.cols[child.header], key=len).data) for child in symbol.head.children]
+ content = "\n".join([e.to_rst() for e in inner])
+ return f"+-{"-+-".join([t*"-" for t in top])}-+\n{content}\n+={"=+=".join([t*"=" for t in top])}=+"
+
+class TBodyRST(CustomRst['TBody']):
+ def to_rst(self, inner, symbol, parent, **kwargs):
+ bottom = [len(max(symbol.table.cols[child.header], key=len).data) for child in symbol.table.head.head.children]
+ return f'{f"\n+-{"-+-".join(["-"*b for b in bottom])}-+\n".join([e.to_rst() for e in inner if isinstance(e, Tr)])}\n+-{"-+-".join(["-"*b for b in bottom])}-+'
+
+class TrRST(CustomRst['Tr']):
+ def to_rst(self, inner, symbol, parent, **kwargs):
+ return f'| {" |\n| ".join(" | ".join([e.to_rst() for e in inner]).split("\n"))} |'
+
+
+class TdRST(CustomRst['Td']):
+ def to_rst(self, inner, symbol, parent, **kwargs):
content = " ".join([e.to_rst() for e in inner])
- return content
+ width = len(max(symbol.table.cols[symbol.header], key=len).data)
+ return content.center(width)
-class TrRST(CustomRst):
- def to_rst(self, inner, symbol, parent):
- # Get cell contents
- cells = [c.to_rst() for c in inner]
-
- # Calculate column widths
- widths = [len(cell) for cell in cells]
-
- # Create row with proper spacing
- row = "| " + " | ".join(cell.ljust(width) for cell, width in zip(cells, widths)) + " |"
-
- # For header rows, create separator
- if symbol.is_header:
- separator = "+" + "+".join("="*(width + 2) for width in widths) + "+"
- else:
- separator = "+" + "+".join("-"*(width + 2) for width in widths) + "+"
-
- return f"{row}\n{separator}"
+class ThRST(CustomRst['Th']):
+ def to_rst(self, inner, symbol, parent, **kwargs):
+ content = " ".join([e.to_rst() for e in inner])
+ width = len(max(symbol.table.cols[symbol.header], key=len).data)
+ if content == "":
+ return "".center(width)
+ return f"**{content}**".center(width)
-class TdRST(CustomRst):
- def to_rst(self, inner: list[Symbol], symbol: Symbol, parent: Symbol) -> str:
- if len(inner) > 1 or not isinstance(inner[0], (Text, H1, H2, H3, H4, H5, H6)):
- raise TypeError("Table Data may only contain text as inner")
+class ThRST(CustomRst):
+ def to_rst(self, inner, symbol, parent):
+ return " ".join([e.to_rst() for e in inner])
- return inner[0].to_rst()
+class TBodyRST(CustomRst):
+ def to_rst(self, inner, symbol, parent):
+ # This is now handled by TableRST
+ return ""
class Table(Symbol):
+ # All deprecated
+ prop_list = ["align", "bgcolor", "border", "cellpadding", "cellspacing", "frame", "rules", "summary", "width"]
+
html = "table"
md = TableMD()
rst = TableRST()
- nl = True
+ head:'THead' = None
+ body:'TBody' = None
+ foot:'TFoot' = None
+
+ cols: 'dict[Th, list[Td]]' = {}
+ headers: 'list[Th]' = []
+
+ def to_pandas(self):
+ if not self.prepared:
+ self.prepare()
+
+ logger.debug("Converting Table to pandas DataFrame")
+ try:
+ import pandas as pd
+ df = pd.DataFrame([e.to_pandas() for e in self.body.children], columns=self.head.to_pandas())
+ logger.debug(f"Successfully converted table to DataFrame with shape {df.shape}")
+ return df
+ except ImportError:
+ logger.error("pandas not installed - tables extra required")
+ raise ImportError("`tables` extra is required to use `to_pandas`")
+ except Exception as e:
+ logger.error(f"Error converting table to pandas: {str(e)}")
+ raise
+
+ @classmethod
+ def from_pandas(cls, df:'pd.DataFrame'):
+ logger.debug(f"Creating Table from pandas DataFrame with shape {df.shape}")
+ try:
+ import pandas as pd
+ self = cls()
+ head = THead.from_pandas(df.columns)
+ body = TBody.from_pandas(df)
+
+ self.head = head
+ self.body = body
+
+ self.add_child(head)
+ self.add_child(body)
+
+ logger.debug("Successfully created Table from DataFrame")
+ logger.debug(f"Table has {len(self.head.children)} columns and {len(self.body.children)} rows with shape {df.shape}")
+ logger.debug(f"Table head: {self.head.to_list()}")
+ logger.debug(f"Table body: {self.body.to_list()}")
+ logger.debug(f"Table foot: {self.foot.to_list()}")
+ return self
+ except ImportError:
+ logger.error("pandas not installed - tables extra required")
+ raise ImportError("`tables` extra is required to use `from_pandas`")
+ except Exception as e:
+ logger.error(f"Error creating table from pandas: {str(e)}")
+ raise
+
+ def prepare(self, parent = None, *args, **kwargs):
+ return super().prepare(parent, table=self, *args, **kwargs)
+
+class THead(Symbol):
+ html = "thead"
+ rst = THeadRST()
+ md = THeadMD()
+
+ table:'Table' = None
+ data:'list[Tr]' = None
+
+
+ def to_pandas(self) -> 'pd.Index':
+ import pandas as pd
+ if len(self.data) == 0:
+ pass # Return undefined
+
+ elif len(self.data) == 1:
+ return pd.Index([d.data for d in self.data])
+
+ def to_list(self) -> 'list[list[str]]':
+ if not self.prepared:
+ self.prepare()
+
+ return [
+ [
+ d.data for d in row.data
+ ] for row in self.data
+ ]
+
+ @classmethod
+ def from_pandas(cls, data:'pd.Index | pd.MultiIndex'):
+ self = cls()
+
+ self.add_child(Tr.from_pandas(data))
+
+ @classmethod
+ def from_list(cls, data:'list[str]|list[list[str]]'):
+ self = cls()
+ if isinstance(data[0], list):
+ self.extend_children([Tr.from_list(d, head=True) for d in data])
+ else:
+ self.add_child(Tr.from_list(data))
+
+ return self
+
+ def prepare(self, parent = None, table=None, *args, **kwargs):
+ assert isinstance(table, Table)
+ self.table = table
+ self.table.head = self
+ return super().prepare(parent, table=table, head=self, *args, **kwargs)
+
+class TBody(Symbol):
+ html = "tbody"
+ rst = TBodyRST()
+ md = TBodyMD()
+
+ table:'Table' = None
+ data :'list[Tr]' = []
+
+ def to_pandas(self):
+ if not self.prepared:
+ self.prepare()
+
+ logger.debug("Converting TBody to pandas format")
+ data = [e.to_pandas() for e in self.children]
+ logger.debug(f"Converted {len(data)} rows from TBody")
+ return data
+
+ @classmethod
+ def from_pandas(cls, df:'pd.DataFrame'):
+ logger.debug(f"Creating TBody from DataFrame with {len(df)} rows")
+ try:
+ self = cls()
+
+ for i, row in df.iterrows():
+ tr = Tr.from_pandas(row)
+ self.children.append(tr)
+ logger.debug(f"Added row {i} to TBody")
+
+ return self
+ except ImportError:
+ logger.error("pandas not installed - tables extra required")
+ raise ImportError("`tables` extra is required to use `from_pandas`")
+
+ @classmethod
+ def from_list(cls, data:'list[list[str]]'):
+ try:
+ self = cls()
+
+ for row in data:
+ self.add_child(Tr.from_list(row))
+
+ except Exception as e:
+ logger.error(f"Exception occurred in `from_list`: {e}")
+
+ def to_list(self):
+ return [
+ row.to_list() for row in self.data
+ ]
+
+ def prepare(self, parent = None, table=None, *args, **kwargs):
+ assert isinstance(table, Table)
+ self.table = table
+ self.table.body = self
+ return super().prepare(parent, table=table, head=self, *args, **kwargs)
+
+class TFoot(Symbol):
+ html = "tfoot"
+ md = TBodyMD()
+ rst = TBodyRST()
+
+ table:'Table' = None
+ data :'list[Tr]' = []
+
+ def to_pandas(self):
+ if not self.prepared:
+ self.prepare()
+
+ logger.debug("Converting TFoot to pandas format")
+ data = [e.to_pandas() for e in self.children]
+ logger.debug(f"Converted {len(data)} rows from TFoot")
+ return data
+
+ @classmethod
+ def from_pandas(cls, df:'pd.DataFrame'):
+ logger.debug(f"Creating TFoot from DataFrame with {len(df)} rows")
+ try:
+ self = cls()
+
+ for i, row in df.iterrows():
+ tr = Tr.from_pandas(row)
+ self.children.append(tr)
+ logger.debug(f"Added row {i} to TFoot")
+
+ return self
+ except ImportError:
+ logger.error("pandas not installed - tables extra required")
+ raise ImportError("`tables` extra is required to use `from_pandas`")
+
+ def prepare(self, parent = None, table=None, *args, **kwargs):
+ assert isinstance(table, Table)
+ self.table = table
+ self.table.foot = self
+ return super().prepare(parent, table=table, head=self, *args, **kwargs)
class Tr(Symbol):
html = "tr"
md = TrMD()
-
- def __init__(self, is_header=False, **kwargs):
- super().__init__(**kwargs)
- self.is_header = is_header
+ rst = TrRST()
+
+ head:'THead|TBody|TFoot' = None
+ table:'Table' = None
+ data:'list[t.Union[Td, Th]]' = []
+
+
+ def to_pandas(self):
+ if not self.prepared:
+ self.prepare()
+
+ if isinstance(self.head, THead):
+ raise ValueError("This `Tr` is a header row and cannot be converted to a pandas `Series`")
+
+ try:
+ import pandas as pd
+
+ return pd.Series(
+ [d for d in self.data],
+ self.table.head.to_pandas()
+ )
+
+ except ImportError:
+ raise ImportError("`tables` extra is required to use `to_pandas`")
+
+ @t.overload
+ @classmethod
+ def from_pandas(cls, series:'pd.Series', head:'t.Literal[False]'=False): ...
+
+ @t.overload
+ @classmethod
+ def from_pandas(cls, series:'pd.Index', head:'t.Literal[True]'): ...
+
+ @classmethod
+ def from_pandas(cls, series:'pd.Series | pd.Index', head:'bool'=False):
+ try:
+ self = cls()
+
+ if head:
+ self.extend_children([Th(inner=[Text(d)]) for d in series])
+
+ self.extend_children([Td(inner=[Text(d)]) for d in series])
+
+ return self
+ except ImportError:
+ raise ImportError("`tables` extra is required to use `from_pandas`")
+
+
+ def to_list(self):
+ if not self.prepared:
+ self.prepare()
+
+ return [e.data for e in self.data]
+
+ @classmethod
+ def from_list(cls, data:'list[str]', head:'bool'=False):
+ self = cls()
+ for value in data:
+ td = Td(inner=[Text(value)]) if not head else Th(inner=[Text(value)])
+ self.children.append(td)
+
+ return self
+
+ def prepare(self, parent = None, table=None, head:'THead|TBody|TFoot'=None, *args, **kwargs):
+ assert isinstance(table, Table)
+ self.table = table
+ if head: self.head = head
+ return super().prepare(parent, table=table, row=self, *args, **kwargs)
class Td(Symbol):
+ prop_list = ["colspan", "rowspan", "headers"]
+ # Deprecated
+ prop_list += ["abbr", "align", "axis", "bgcolor", "char", "charoff", "height", "scope", "valign", "width"]
+
html = "td"
md = TdMD()
rst = TdRST()
+ children:'List[Text]' = List()
+ row:'Tr' = None
+
+ @property
+ def data(self):
+ return self.children.get(0, Text("")).text
+
+ @property
+ def width(self):
+ return len(self.data)
+
+ def prepare(self, parent = None, table=None, row=None, *args, **kwargs):
+ assert isinstance(table, Table)
+ self.table = table
+ self.row = row
+
+ self.row.data.append(self)
+ self.header = self.table.headers[self.row.children.index(self)]
+ self.table.cols[self.header].append(self)
+ return super().prepare(parent, table=table, data=self, *args, **kwargs)
+
+ def __len__(self):
+ return len(self.data)
+
class Th(Symbol):
+ prop_list = ["abbr", "colspan","headers", "rowspan", "scope"]
+ # Deprecated
+ prop_list += ["align", "axis", "bgcolor", "char", "charoff", "height", "valign", "width"]
+
+
html = "th"
- md = TdMD()
- rst = ThRST()
\ No newline at end of file
+ md = ThMD()
+ rst = ThRST()
+
+ children:'List[Text]' = List()
+ row:'Tr' = None
+
+ def __init__(self, styles: dict[str, str] = {}, classes: list[str] = [], dom: bool = True, inner: list[Symbol] = [], **props):
+ super().__init__(styles, classes, dom, inner, **props)
+
+ @property
+ def data(self):
+ contents = self.children.get(0, Text("")).text
+ logger.debug(f"Th data: {contents}")
+ if contents == "":
+ logger.debug("Th data is empty")
+ return ""
+ logger.debug("Th data is not empty")
+ return f"**{contents}**"
+
+ @property
+ def width(self):
+ """Width of the data"""
+ if self.data == "":
+ return 0
+ return len(self.data)-4
+
+ def prepare(self, parent = None, table=None, row=None, *args, **kwargs):
+ assert isinstance(table, Table)
+ self.table = table
+ self.row = row
+
+ self.row.data.append(self)
+ self.header = self
+ self.table.cols[self] = [self]
+ return super().prepare(parent, table=table, data=self, *args, **kwargs)
+
+ def __len__(self):
+ """Width of the element (data + bolding)"""
+ return len(self.data)
diff --git a/BetterMD/elements/template.py b/BetterMD/elements/template.py
new file mode 100644
index 0000000..ef8bc37
--- /dev/null
+++ b/BetterMD/elements/template.py
@@ -0,0 +1,8 @@
+from .symbol import Symbol
+
+class Template(Symbol):
+ prop_list = ["shadowrootmode", "shadowrootclonable", "shadowrootdelegatesfocus", "shadowrootserializable"]
+
+ html = "template"
+ md = ""
+ rst = ""
\ No newline at end of file
diff --git a/BetterMD/elements/text.py b/BetterMD/elements/text.py
index 5a82c9f..cd78a40 100644
--- a/BetterMD/elements/text.py
+++ b/BetterMD/elements/text.py
@@ -6,21 +6,24 @@
# This is not equivelant to the html span or p tags but instead just raw text
class Text(Symbol):
- md = "{t}"
- html = "{t}"
- rst = "{t}"
+ md = "raw_text"
+ html = "raw_text"
+ rst = "raw_text"
- def __init__(self, text:str, dom = True, **props):
+ def __init__(self, text:str, **props):
self.text = text
- return super().__init__(dom=dom, **props)
+ return super().__init__(**props)
- def to_html(self):
+ def to_html(self, indent=0):
+ return f"{' '*indent}{self.text}"
+
+ def to_md(self):
return self.text
- def to_md(self):
+ def to_rst(self):
return self.text
def __str__(self):
- return f"{self.text}"
+ return self.text
- __repr__ = __str__
\ No newline at end of file
+ __repr__ = __str__
diff --git a/BetterMD/elements/text_formatting.py b/BetterMD/elements/text_formatting.py
index e1dcab9..ac21955 100644
--- a/BetterMD/elements/text_formatting.py
+++ b/BetterMD/elements/text_formatting.py
@@ -17,6 +17,11 @@ class Strong(Symbol):
md = SMD()
rst = "**"
+class B(Symbol):
+ html = "b"
+ md = SMD()
+ rst = "**"
+
class Em(Symbol):
html = "em"
md = EMD()
diff --git a/BetterMD/elements/textarea.py b/BetterMD/elements/textarea.py
new file mode 100644
index 0000000..3b4a617
--- /dev/null
+++ b/BetterMD/elements/textarea.py
@@ -0,0 +1,8 @@
+from .symbol import Symbol
+
+class Textarea(Symbol):
+ prop_list = ["autocapitalize", "autocomplete", "autocorrect", "autofocus", "cols", "dirname", "disabled", "form", "maxlength", "minlength", "name", "placeholder", "readonly", "required", "rows", "spellcheck", "wrap"]
+
+ html = "textarea"
+ md = ""
+ rst = ""
\ No newline at end of file
diff --git a/BetterMD/elements/time.py b/BetterMD/elements/time.py
new file mode 100644
index 0000000..86d2255
--- /dev/null
+++ b/BetterMD/elements/time.py
@@ -0,0 +1,8 @@
+from .symbol import Symbol
+
+class Time(Symbol):
+ prop_list = ["datetime"]
+
+ html = "time"
+ md = ""
+ rst = ""
\ No newline at end of file
diff --git a/BetterMD/elements/title.py b/BetterMD/elements/title.py
new file mode 100644
index 0000000..f4f3c0a
--- /dev/null
+++ b/BetterMD/elements/title.py
@@ -0,0 +1,25 @@
+from .symbol import Symbol
+from ..markdown import CustomMarkdown
+from ..rst import CustomRst
+from .text import Text
+
+class MD(CustomMarkdown):
+ def to_md(self, inner: list[Symbol], symbol: Symbol, parent: Symbol, **kwargs) -> str:
+ if not inner or not isinstance(inner[0], Text) or len(inner) != 1:
+ raise ValueError("Title element must contain a single Text element")
+
+ return f'title: "{inner[0].to_md()}"\n# "{inner[0].to_md()}"'
+
+class RST(CustomRst):
+ def to_rst(self, inner: list[Symbol], symbol: Symbol, parent: Symbol, **kwargs) -> str:
+ if not inner or not isinstance(inner[0], Text) or len(inner) != 1:
+ raise ValueError("Title element must contain a single Text element")
+
+ return f":title: {inner[0].to_rst()}"
+
+class Title(Symbol):
+ prop_list = ["align", "bgcolor", "char", "charoff", "valign"]
+
+ html = "title"
+ md = MD()
+ rst = RST()
diff --git a/BetterMD/elements/track.py b/BetterMD/elements/track.py
new file mode 100644
index 0000000..b23a7cf
--- /dev/null
+++ b/BetterMD/elements/track.py
@@ -0,0 +1,8 @@
+from .symbol import Symbol
+
+class Track(Symbol):
+ prop_list = ["default", "kind", "label", "src", "srclang"]
+
+ html = "track"
+ md = ""
+ rst = ""
\ No newline at end of file
diff --git a/BetterMD/elements/tt.py b/BetterMD/elements/tt.py
new file mode 100644
index 0000000..4c406d0
--- /dev/null
+++ b/BetterMD/elements/tt.py
@@ -0,0 +1,6 @@
+from .symbol import Symbol
+
+class TT(Symbol):
+ html = "tt"
+ md = ""
+ rst = ""
\ No newline at end of file
diff --git a/BetterMD/elements/u.py b/BetterMD/elements/u.py
new file mode 100644
index 0000000..90aa048
--- /dev/null
+++ b/BetterMD/elements/u.py
@@ -0,0 +1,6 @@
+from .symbol import Symbol
+
+class U(Symbol):
+ html = "u"
+ md = ""
+ rst = ""
\ No newline at end of file
diff --git a/BetterMD/elements/var.py b/BetterMD/elements/var.py
new file mode 100644
index 0000000..af4ebe4
--- /dev/null
+++ b/BetterMD/elements/var.py
@@ -0,0 +1,6 @@
+from .symbol import Symbol
+
+class Var(Symbol):
+ html = "var"
+ md = ""
+ rst = ""
\ No newline at end of file
diff --git a/BetterMD/elements/video.py b/BetterMD/elements/video.py
new file mode 100644
index 0000000..ac5ef53
--- /dev/null
+++ b/BetterMD/elements/video.py
@@ -0,0 +1,6 @@
+from .symbol import Symbol
+
+class Video(Symbol):
+ html = "video"
+ md = ""
+ rst = ""
\ No newline at end of file
diff --git a/BetterMD/elements/wbr.py b/BetterMD/elements/wbr.py
new file mode 100644
index 0000000..a747533
--- /dev/null
+++ b/BetterMD/elements/wbr.py
@@ -0,0 +1,6 @@
+from .symbol import Symbol
+
+class WBR(Symbol):
+ html = "wbr"
+ md = ""
+ rst = ""
\ No newline at end of file
diff --git a/BetterMD/elements/xmp.py b/BetterMD/elements/xmp.py
new file mode 100644
index 0000000..68862b4
--- /dev/null
+++ b/BetterMD/elements/xmp.py
@@ -0,0 +1,6 @@
+from .symbol import Symbol
+
+class XMP(Symbol):
+ html = "xmp"
+ md = ""
+ rst = ""
\ No newline at end of file
diff --git a/BetterMD/html/custom_html.py b/BetterMD/html/custom_html.py
index b6fc759..b1e7568 100644
--- a/BetterMD/html/custom_html.py
+++ b/BetterMD/html/custom_html.py
@@ -1,13 +1,15 @@
import typing as t
+from abc import ABC, abstractmethod
if t.TYPE_CHECKING:
from ..elements.symbol import Symbol
T = t.TypeVar("T", default='Symbol')
-class CustomHTML(t.Generic[T]):
+class CustomHTML(t.Generic[T], ABC):
+ @abstractmethod
def to_html(self, inner:'list[Symbol]', symbol:'T', parent:'Symbol') -> str: ...
- def prepare(self, inner:'list[Symbol]', symbol:'T', parent:'Symbol'):...
+ def prepare(self, inner:'list[Symbol]', symbol:'T', parent:'Symbol', *args, **kwargs) -> 'list[Symbol]': ...
def verify(self, text) -> bool: ...
\ No newline at end of file
diff --git a/BetterMD/markdown/custom_markdown.py b/BetterMD/markdown/custom_markdown.py
index 53ab7c9..3cdc83d 100644
--- a/BetterMD/markdown/custom_markdown.py
+++ b/BetterMD/markdown/custom_markdown.py
@@ -1,16 +1,18 @@
import typing as t
+from abc import ABC, abstractmethod
if t.TYPE_CHECKING:
from ..elements.symbol import Symbol
T = t.TypeVar("T", default='Symbol')
-class CustomMarkdown(t.Generic[T]):
+class CustomMarkdown(t.Generic[T], ABC):
prop = ""
md: 'dict[str, str]' = {}
- def to_md(self, inner: 'list[Symbol]', symbol:'T', parent:'Symbol') -> str: ...
+ @abstractmethod
+ def to_md(self, inner: 'list[Symbol]', symbol:'T', parent:'Symbol') -> 'str': ...
- def prepare(self, inner:'list[Symbol]', symbol:'T', parent:'Symbol'): ...
+ def prepare(self, inner:'list[Symbol]', symbol:'T', parent:'Symbol', *args, **kwargs) -> 'list[Symbol]': ...
- def verify(self, text) -> bool: ...
\ No newline at end of file
+ def verify(self, text) -> 'bool': ...
diff --git a/BetterMD/parse/__init__.py b/BetterMD/parse/__init__.py
new file mode 100644
index 0000000..187ceae
--- /dev/null
+++ b/BetterMD/parse/__init__.py
@@ -0,0 +1,6 @@
+from .collection import Collection
+from .typing import ELEMENT, TEXT
+from .html import HTMLParser
+from .markdown import MDParser
+
+__all__ = ["Collection", "HTMLParser", "MDParser", "ELEMENT", "TEXT"]
\ No newline at end of file
diff --git a/BetterMD/parse/collection.py b/BetterMD/parse/collection.py
new file mode 100644
index 0000000..cccf4b2
--- /dev/null
+++ b/BetterMD/parse/collection.py
@@ -0,0 +1,27 @@
+import typing as t
+import logging
+from ..html import CustomHTML
+
+if t.TYPE_CHECKING:
+ from ..elements import Symbol
+
+class Collection:
+ def __init__(self, *symbols:'type[Symbol]'):
+ self.symbols = list(symbols)
+ self.logger = logging.getLogger("BetterMD")
+
+ def add_symbols(self, symbol:'type[Symbol]'):
+ self.symbols.append(symbol)
+
+ def remove_symbol(self, symbol:'type[Symbol]'):
+ self.symbols.remove(symbol)
+
+ def find_symbol(self, name:'str', raise_errors:'bool'=False) -> 't.Optional[type[Symbol]]':
+ for symbol in self.symbols:
+ if symbol.__qualname__.lower() == name.lower():
+ return symbol
+
+
+ if raise_errors:
+ raise ValueError(f"Symbol `{name}` not found in collection, if using default symbols it may not be supported.")
+ return None
\ No newline at end of file
diff --git a/BetterMD/parse/html.py b/BetterMD/parse/html.py
new file mode 100644
index 0000000..7fb0412
--- /dev/null
+++ b/BetterMD/parse/html.py
@@ -0,0 +1,204 @@
+from .typing import ELEMENT, TEXT
+from ..typing import ATTRS
+import typing as t
+
+class HTMLParser:
+ NON_PARSING_TAGS = ['script', 'style', 'textarea']
+
+ def __init__(self):
+ self.reset()
+
+ @property
+ def children(self):
+ return self.current_tag["children"]
+
+ def reset(self):
+ self.dom:'list[ELEMENT|TEXT]' = []
+ self.buffer = ""
+ self.state = ""
+ self.current_tag = {
+ "type": "element",
+ "name": "dom",
+ "attributes": {},
+ "children": self.dom,
+ "parent": None
+ }
+ self.tag = ""
+ self.non_parsing_content = ""
+ self.in_non_parsing_tag = False
+ self.current_non_parsing_tag = None
+
+ def create_element(self, name:'str', attrs:'ATTRS'=None, children:'list[ELEMENT|TEXT]'=None) -> 'ELEMENT':
+ if children is None:
+ children = []
+
+ if attrs is None:
+ attrs = {}
+
+ return {
+ "type": "element",
+ "name": name,
+ "attributes": attrs,
+ "children": children,
+ "parent": self.current_tag
+ }
+
+ @staticmethod
+ def create_text(content:'str') -> 'TEXT':
+ return {
+ "type": "text",
+ "content": content,
+ "name": "text"
+ }
+
+ def parse(self, html:'str') -> 'list[ELEMENT]':
+ self.reset()
+ i = 0
+
+ while i < len(html):
+ char = html[i]
+ if self.in_non_parsing_tag:
+ closing_tag = f"{self.current_non_parsing_tag}>"
+ if html[i:i+len(closing_tag)].lower() == closing_tag.lower():
+ # Found closing tag, create element with unparsed content
+ self.children.append(self.create_text(self.non_parsing_content))
+ self.current_tag = self.current_tag["parent"]
+
+ self.in_non_parsing_tag = False
+ self.current_non_parsing_tag = None
+ self.non_parsing_content = ""
+ i += len(closing_tag)
+ else:
+ self.non_parsing_content += char
+ i += 1
+ continue
+
+ elif char == '<':
+ if self.buffer:
+ self.children.append(self.create_text(self.buffer))
+ self.buffer = ""
+
+ # Check for comment
+ if html[i:i+4] == '
+ while i < len(html) - 2:
+ if html[i:i+3] == '-->':
+ break
+ comment += html[i]
+ i += 1
+
+ # Create comment element
+ self.children.append(self.create_element("comment", children=[self.create_text(comment)]))
+
+ return i + 3 # Skip past -->
diff --git a/BetterMD/parse/markdown/__init__.py b/BetterMD/parse/markdown/__init__.py
new file mode 100644
index 0000000..2b8eaeb
--- /dev/null
+++ b/BetterMD/parse/markdown/__init__.py
@@ -0,0 +1,4 @@
+from .extensions import BaseExtension, Extension
+from .parser import MDParser
+
+MDParser.add_extension(BaseExtension)
\ No newline at end of file
diff --git a/BetterMD/parse/markdown/extensions/__init__.py b/BetterMD/parse/markdown/extensions/__init__.py
new file mode 100644
index 0000000..14f9707
--- /dev/null
+++ b/BetterMD/parse/markdown/extensions/__init__.py
@@ -0,0 +1,4 @@
+from .base import BaseExtension
+from .extension import Extension
+
+__all__ = ["BaseExtension", "Extension"]
\ No newline at end of file
diff --git a/BetterMD/parse/markdown/extensions/base.py b/BetterMD/parse/markdown/extensions/base.py
new file mode 100644
index 0000000..9646c30
--- /dev/null
+++ b/BetterMD/parse/markdown/extensions/base.py
@@ -0,0 +1,672 @@
+import re
+import typing as t
+from .extension import Extension
+
+if t.TYPE_CHECKING:
+ from ..parser import MDParser
+ from ...typing import ELEMENT, TEXT
+ from ..typing import ELM_TYPE_W_END, ELM_TYPE_WO_END, OL_LIST, UL_LIST, LIST_ITEM, LIST_TYPE, OL_TYPE, UL_TYPE
+
+
+def unescape(text: str) -> 'str':
+ """Unescape text."""
+ for i in range(len(text)):
+ m = re.match(r'^\\(.)', text[i:])
+ if m:
+ text = text[:i] + m.group(1) + text[i + m.end(0):]
+ return text
+
+def dequote(text: str) -> str:
+ """Remove quotes from text."""
+ if text.startswith('"') and text.endswith('"'):
+ return text[1:-1]
+ elif text.startswith("'") and text.endswith("'"):
+ return text[1:-1]
+ return text
+
+def count(text: 'str', char: 'str') -> 'int':
+ return len(re.findall(r"(? 'dict[str, ELM_TYPE_W_END | ELM_TYPE_WO_END]':
+ return {
+ "blockquote": {
+ "pattern": r"^> (.*)$",
+ "handler": self.handle_blockquote,
+ "end": self.end_blockquote
+ },
+ "code": {
+ "pattern": r"^```([A-Za-z]*)?$",
+ "handler": self.handle_code,
+ "end": self.end_code
+ },
+ "h": {
+ "pattern": r"^(#{1,6})(?: (.*))?$",
+ "handler": self.handle_h,
+ "end": None
+ },
+ "hr": {
+ "pattern": r"^---+$",
+ "handler": self.handle_hr,
+ "end": None
+ },
+ "br": {
+ "pattern": r"^\s*$",
+ "handler": self.handle_br,
+ "end": None
+ },
+ "ul": {
+ "pattern": r"^(\s*)(-|\+|\*)(?: +(?:\[( |x|X)\])?(.*))?$",
+ "handler": self.handle_ul,
+ "end": self.end_list
+ },
+ "ol": {
+ "pattern": r"^(\s*)(\d)(\.|\))(?: +(?:\[( |x|X)\] *)?(.*)?)?$",
+ "handler": self.handle_ol,
+ "end": self.end_list
+ },
+ "thead": {
+ "pattern": r"^\|(?::?-+:?\|)+$",
+ "handler": self.handle_thead,
+ "end": self.end_table
+ },
+ "tr": {
+ "pattern": r"^\|(?:[^|\n]+\|)+$",
+ "handler": self.handle_tr,
+ "end": self.end_table
+ },
+ "title": {
+ "pattern": r"^title:(?: (.+))?$",
+ "handler": self.handle_title,
+ "end": None
+ }
+ }
+
+ @property
+ def text_tags(self):
+ return {
+ "inline_link": {
+ "pattern": r"(?]+)>",
+ "handler": self.automatic_link
+ },
+ "reference_definition": {
+ "pattern": r"^\[([^\]]+)\]:\s*([^\s]+)",
+ "handler": self.reference_definition
+ },
+ "reference": {
+ "pattern": r"^\[([^\]]+)\]\[([^\]]+)\]\s*",
+ "handler": self.reference
+ },
+ "image": {
+ "pattern": r"^!\[",
+ "handler": self.image
+ },
+ "bold_and_italic": {
+ "pattern": [r"^([\*_])([\*_]){2}([^\*\n\r]+?)\2{2}\1", r"^([\*_]){2}([\*_])([^\*\n\r]+?)\2\1{2}"],
+ "handler": self.bold_and_italic
+ },
+ "bold": {
+ "pattern": r"^([\*_])\1{1}(.+?)\1{2}",
+ "handler": self.bold
+ },
+ "italic": {
+ "pattern": r"^([\*_])([^\*\n\r]+?)\1",
+ "handler": self.italic
+ },
+ "code": {
+ "pattern": r"^(`+)([\s\S]*?)\1",
+ "handler": self.code
+ }
+ }
+
+ #################### Handlers ####################
+
+ ########## Paragraph Handlers ##########
+
+ def inline_link(self, text:'str'):
+ def handle_alt(text:'str'):
+ ob = 0
+ alt = ""
+ i = 0
+
+ for i, char in enumerate(text):
+ if char == "[":
+ ob += 1
+ elif char == "]":
+ ob -= 1
+ if ob == 0:
+ break
+
+ alt += char
+
+ return alt[1:], i+1
+
+ def handle_link(text:'str'):
+ i = 1
+ link = ""
+ link_mode = None # True - <>, False - " "
+ obs = 0
+ esc = False
+ while i < len(text):
+ char = text[i]
+ if link_mode is None:
+ if char == "<":
+ link_mode = True
+ obs = 1
+ else:
+ link_mode = False
+ link = char
+
+ elif esc:
+ esc = False
+ link += char
+
+ elif char == "\\":
+ esc = True
+
+ elif link_mode:
+ if char == "<":
+ obs += 1
+ if obs != 1:
+ link += char
+ elif char == ">":
+ obs -= 1
+ if obs == 0:
+ return link, i+1
+ else:
+ link += char
+ else:
+ link += char
+
+ elif char == " ":
+ return link, i
+
+ elif char == ")":
+ return link, i
+
+ else:
+ link += char
+
+ i += 1
+ return link, i
+
+ def handle_title(text:'str'):
+ i = text[1:].index(text[0])
+ return text[1:i+1], i+1
+
+ assert len(text) >= 4
+
+ if text[1] != "]":
+ alt, i = handle_alt(text)
+ else:
+ alt = None
+ i = 2
+ if text[i+1] in ["'", '"']:
+ href = None
+ index = 0
+
+ elif text[i+1] != ")":
+ href, index = handle_link(text[i:])
+ else:
+ href = None
+ index = 1
+ i += index
+
+ if len(text)-1 <= i or text[i+1] not in ['"', "'"]:
+ title = None
+
+ else:
+ title, index = handle_title(text[i+1:])
+ i += index + 2
+
+ el = self.create_element("a", {"class": "inline-link", **({"href": href} if href else {}), **({"title": title} if title is not None else {})}, [self.create_text(alt)] if alt else [])
+ return el, i
+
+ def automatic_link(self, text:'str'):
+ match = re.match(self.text_tags["automatic_link"]["pattern"], text)
+
+ assert match is not None, "Automatic link not found"
+
+ url = match.group(1)
+ return self.create_element("a", {"class": "automatic-link", "href": url}, [self.create_text(url)]), match.end() - match.start()
+
+ def reference_definition(self, text:'str'):
+ match = re.match(self.text_tags["reference_definition"]["pattern"], text)
+ assert match is not None, "Reference definition not found"
+
+ label = match.group(1)
+ url = match.group(2)
+ return self.create_element("a", {"class": ["ref-def"], "href": url, "ref": True, "refId":label}, [self.create_text(label)])
+
+ def reference(self, text:'str'):
+ match = re.match(self.text_tags["reference"]["pattern"], text)
+ assert match is not None, "Reference not found"
+
+ label = match.group(1)
+ ref = match.group(2)
+ return self.create_element("a", { "class": ["ref"], "ref": True, "refId":ref }, [self.create_text(label)])
+
+ def image(self, text:'str'):
+ def handle_alt(text:'str'):
+ ob = 0
+ alt = ""
+ i = 0
+
+ for i, char in enumerate(text):
+ if char == "[":
+ ob += 1
+ elif char == "]":
+ ob -= 1
+ if ob == 0:
+ break
+
+ alt += char
+
+ return alt[1:], i+1
+
+ def handle_link(text:'str'):
+ i = 1
+ link = ""
+ link_mode = None # True - <>, False - " "
+ obs = 0
+ esc = False
+ while i < len(text):
+ char = text[i]
+ if link_mode is None:
+ if char == "<":
+ link_mode = True
+ obs = 1
+ else:
+ link_mode = False
+ link = char
+
+ elif esc:
+ esc = False
+ link += char
+
+ elif char == "\\":
+ esc = True
+
+ elif link_mode:
+ if char == "<":
+ obs += 1
+ if obs != 1:
+ link += char
+ elif char == ">":
+ obs -= 1
+ if obs == 0:
+ return link, i+1
+ else:
+ link += char
+ else:
+ link += char
+
+ elif char == " ":
+ return link, i
+
+ elif char == ")":
+ return link, i
+
+ else:
+ link += char
+
+ i += 1
+ return link, i
+
+ def handle_title(text:'str'):
+ i = text[1:].index(text[0])
+ return text[1:i+1], i+1
+
+ assert len(text) >= 5
+
+ if text[1] != "]":
+ alt, i = handle_alt(text)
+ else:
+ alt = None
+ i = 2
+ if text[i+1] in ["'", '"']:
+ href = None
+ index = 0
+
+ elif text[i+1] != ")":
+ href, index = handle_link(text[i:])
+ else:
+ href = None
+ index = 1
+ i += index
+
+ if len(text)-1 <= i or text[i+1] not in ['"', "'"]:
+ title = None
+
+ else:
+ title, index = handle_title(text[i+1:])
+ i += index + 2
+
+ el = self.create_element("img", {**({"href": href} if href else {}), **({"title": title} if title is not None else {})}, [self.create_text(alt)] if alt else [])
+ return el, i
+
+ def bold(self, text:'str'):
+ match = re.match(self.text_tags["bold"]["pattern"], text)
+ assert match is not None, "Bold not found"
+
+ content = match.group(2)
+ return self.create_element("strong", children=self.parse_text(content)), match.end() - match.start()
+
+ def italic(self, text:'str'):
+ match = re.match(self.text_tags["italic"]["pattern"], text)
+ assert match is not None, "Italic not found"
+
+ content = match.group(2)
+ return self.create_element("em", children=self.parse_text(content)), match.end() - match.start()
+
+ def bold_and_italic(self, text:'str'):
+ m1 = re.match(self.text_tags["bold_and_italic"]["pattern"][0], text)
+ m2 = re.match(self.text_tags["bold_and_italic"]["pattern"][1], text)
+ match = m1 or m2
+ assert match is not None, "Bold and italic not found"
+
+ content = match.group(3)
+ return self.create_element("strong", {"class": ["italic-bold" if m1 else "bold-italic"]}, children=[self.create_element("em", children=self.parse_text(content))]), match.end() - match.start()
+
+ def code(self, text:'str'):
+ match = re.match(self.text_tags["code"]["pattern"], text)
+ assert match is not None, "Code not found"
+
+ return self.create_element("code", {"class": ["codespan"]}, [self.create_text(match.group(2))]), match.end() - match.start()
+
+ ########## Top Level Handlers ##########
+
+ # Blockquote
+
+ def handle_blockquote(self, line: 'str'):
+ if self.block != "BLOCKQUOTE":
+ self.start_block("BLOCKQUOTE", self.end_blockquote)
+
+ match = re.match(self.top_level_tags["blockquote"]["pattern"], line)
+ assert match is not None, "Blockquote not found"
+
+ self.handle_text(match.group(1))
+
+ def end_blockquote(self):
+ subparser = self.parser_class()
+ children = subparser.parse(self.buffer)
+ return self.create_element("blockquote", children=children)
+
+ # Code
+
+ def handle_code(self, line: 'str'):
+ if not self.parsing[0]:
+ self.end_block(parse=False)
+ else:
+ self.parsing = False, ["code"]
+
+ if self.block is None or not self.block.startswith("CODE:"):
+ match = re.match(self.top_level_tags["code"]["pattern"], line)
+ assert match is not None, "Code block not found"
+ lang = match.group(1) or ""
+ self.start_block(f"CODE:{lang}", self.end_code)
+ else:
+ self.end_block()
+
+ def end_code(self):
+ lang = self.block[5:]
+ elm = self.create_element("code", {"class": ["codeblock"], "language": lang}, [self.create_element("pre", children=[self.create_text(self.buffer)])])
+ self.buffer = ""
+ return elm
+
+ # List
+
+ def handle_ul(self, line: 'str'):
+ match = re.match(self.top_level_tags["ul"]["pattern"], line)
+ assert match is not None, "UL not found"
+
+ indent = len(match.group(1))
+ type = match.group(2)
+ input = match.group(3) != None
+ checked = match.group(3) != " "
+ contents = match.group(4) or ""
+
+ if self.block != "UL":
+ self.start_block("UL", self.end_list)
+ self.list_stack = []
+
+ # Store the indent level and content for proper nesting
+ self.list_stack.append({"list":"ul", "input":input, "checked": checked, "indent": indent, "contents": contents, "type": type})
+
+ def handle_ol(self, line: 'str'):
+ match = re.match(self.top_level_tags["ol"]["pattern"], line)
+ assert match is not None, "OL not found"
+
+ indent = len(match.group(1))
+ num = int(match.group(2))
+ type = match.group(3)
+ input = match.group(4) != None
+ checked = match.group(4) != " "
+ contents = match.group(5) or ""
+ input = False
+
+ if self.block != "OL":
+ self.start_block("OL", self.end_list)
+ self.list_stack = []
+
+ # Store the indent level and content for proper nesting
+ self.list_stack.append({"list":"ol", "input":input, "checked": checked, "indent": indent, "contents": contents, "type": type, "num": num})
+
+
+ def end_list(self):
+ def data2li(item: 'UL_LIST|OL_LIST') -> 'LIST_ITEM':
+ return {
+ "data": item,
+ "dataType": "item"
+ }
+
+ lists:'list[LIST_TYPE]' = [{
+ "value": [],
+ "parent": None,
+ "type": "ul" if self.block == "UL" else "ol",
+ "key": self.list_stack[0]["type"],
+ "dataType": "list",
+ **({"start": self.list_stack[0]["num"]} if self.block == "OL" else {})
+ }]
+
+ cur_indent = -1
+ cur_list = lists[0]
+
+ list_modes:'dict[str, t.Literal["ul", "ol"]]' = {
+ "-": "ul", "*": "ul", "+": "ul",
+ ")": "ol", ".": "ol"
+ }
+
+ for item in self.list_stack:
+ indent = item["indent"]
+ mode = list_modes[item["type"]]
+
+ if indent > cur_indent:
+ # Create new nested list
+ new_list:'LIST_TYPE' = {
+ "value": [data2li(item)],
+ "parent": cur_list,
+ "type": mode,
+ "key": item["type"],
+ "dataType": "list",
+ **({"start": item["num"]} if mode == "ol" else {})
+ }
+ cur_list["value"].append(new_list)
+ cur_list = new_list
+ cur_indent = indent
+
+ elif indent < cur_indent:
+ # Go back up the tree
+ while cur_indent > indent and cur_list["parent"] is not None:
+ cur_list = cur_list["parent"]
+ cur_indent -= 1
+ cur_list["value"].append(data2li(item))
+
+ else:
+ # Same level
+ cur_list["value"].append(data2li(item))
+
+ def handle_item(item:'LIST_ITEM'):
+ if item["data"]["input"]:
+ return self.create_element(
+ "li",
+ children=[
+ self.create_element(
+ "input",
+ {
+ "class": ["checklist"],
+ "type": "checkbox",
+ "checked": item["data"]["checked"]
+ }
+ ),
+ self.create_text(item["data"]["contents"])
+ ]
+ )
+ return self.create_element(
+ "li",
+ children=[self.create_text(item["data"]["contents"])]
+ )
+
+ def handle_child_list(child_list:'LIST_TYPE'):
+ return self.create_element(
+ child_list["type"],
+ {
+ "class": [f"list-{child_list['key']}"],
+ **({"start": child_list["start"]} if child_list["type"] == "ol" else {})
+ },
+ [
+ handle_child_list(child) if child.get("dataType") == "list"
+ else handle_item(child)
+ for child in child_list["value"]
+ ]
+ )
+
+ return [handle_child_list(list) for list in lists]
+
+ # Table
+
+ # TR
+
+ def handle_tr(self, line: 'str'):
+ if self.had_thead:
+ self.table.append(line)
+ return
+
+ if self.thead:
+ self.handle_text(self.thead)
+
+ self.thead = line
+
+
+ def handle_thead(self, line: 'str'):
+ if not self.thead:
+ self.handle_text(line)
+ elif self.had_thead:
+ self.table.append(line)
+ else:
+ self.start_block("TABLE", self.end_table)
+ self.had_thead = True
+ self.tcols = []
+ for col in line.split("|"):
+ if col == "":
+ continue
+ if col.startswith(":") and col.endswith(":"):
+ self.tcols.append("center")
+ elif col.startswith(":"):
+ self.tcols.append("left")
+ elif col.endswith(":"):
+ self.tcols.append("right")
+ else:
+ self.tcols.append("justify")
+
+ def end_table(self):
+ head = [h.strip() for h in self.thead.removeprefix("|").removesuffix("|").split("|")]
+
+ body = [
+ [
+ cell.strip() for cell in row.removeprefix("|").removesuffix("|").split("|")
+ ] for row in self.table
+ ]
+
+ return self.create_element(
+ "table",
+ children=[
+ self.create_element(
+ "thead",
+ children=[
+ self.create_element(
+ "tr",
+ children=[
+ self.create_element("th", {"class": [f"list-{style}"]}, [self.create_text(cell.strip())]) for cell, style in zip(head, self.tcols)
+ ]
+ )
+ ]
+ ),
+ self.create_element(
+ "tbody",
+ children=[
+ self.create_element(
+ "tr",
+ children=[
+ self.create_element("td", {"class": [f"list-{style}"]},[self.create_text(cell.strip())]) for cell, style in zip(row, self.tcols)
+ ]
+ ) for row in body
+ ]
+ )
+ ]
+ )
+
+ # Br
+
+ def handle_br(self, line: 'str'):
+ self.end_block()
+ return self.create_element("br")
+
+ # Header
+
+ def handle_h(self, line: 'str'):
+ self.end_block()
+ match = re.match(self.top_level_tags["h"]["pattern"], line)
+ assert match is not None, "Header not found"
+
+ level = len(match.group(1))
+ content = match.group(2)
+
+ return self.create_element(f"h{level}", {"id": content.replace(" ", "-")}, children=[self.create_text(content)])
+
+ # Horizontal rule
+
+ def handle_hr(self, line: 'str'):
+ self.end_block()
+ return self.create_element("hr", {})
+
+ def handle_title(self, line: 'str'):
+ self.end_block()
+ match = re.match(self.top_level_tags["title"]["pattern"], line)
+ assert match is not None, "Title not found"
+
+ title = match.group(1)
+ self.head = self.create_element("head", children=[self.create_element("title", children=[self.create_text(title)])])
diff --git a/BetterMD/parse/markdown/extensions/extension.py b/BetterMD/parse/markdown/extensions/extension.py
new file mode 100644
index 0000000..c25de54
--- /dev/null
+++ b/BetterMD/parse/markdown/extensions/extension.py
@@ -0,0 +1,73 @@
+import typing as t
+from abc import ABC, abstractmethod
+
+if t.TYPE_CHECKING:
+ from ..typing import ELM_TYPE_W_END, ELM_TYPE_WO_END, ELM_TEXT, ELEMENT, TEXT
+ from ..parser import MDParser
+
+class Extension(ABC):
+ def __init__(self, parser_class:'type[MDParser]'):
+ self.parser_class = parser_class
+
+ def init(self, parser:'MDParser'):
+ self.parser = parser
+ self.dom = parser.dom
+
+ @property
+ @abstractmethod
+ def name(self) -> 'str': ...
+
+ @property
+ @abstractmethod
+ def top_level_tags(self) -> 'dict[str, ELM_TYPE_W_END | ELM_TYPE_WO_END]':
+ ...
+
+ @property
+ @abstractmethod
+ def text_tags(self) -> 'dict[str, ELM_TEXT]':
+ ...
+
+ @property
+ def buffer(self) -> 'str':
+ return self.parser.buffer
+
+ @buffer.setter
+ def buffer(self, value:'str'):
+ self.parser.buffer = value
+
+ @property
+ def block(self) -> 't.Optional[str]':
+ return self.parser.block
+
+ @block.setter
+ def block(self, value:'str'):
+ self.parser.block = value
+
+ @property
+ def parsing(self) -> 'tuple[bool, list[str]]':
+ return self.parser.parsing
+
+ @parsing.setter
+ def parsing(self, value:'tuple[bool, list[str]]'):
+ self.parser.parsing = value
+
+ def create_text(self, content:'str'):
+ return self.parser.create_text(content)
+
+ def create_element(self, name:'str', attrs:'dict[str, t.Union[str, bool, int, float, list, dict]]'=None, children:'list[ELEMENT|TEXT]'=None):
+ return self.parser.create_element(name, attrs, children)
+
+ def start_block(self, block:'str', end_func:'t.Optional[t.Callable[[], None]]'=None):
+ self.parser.start_block(block, end_func)
+
+ def end_block(self, parse=True):
+ self.parser.end_block(parse)
+
+ def handle_text(self, line:'str'):
+ self.parser.handle_text(line)
+
+ def parse(self, text:'str'):
+ return self.parser.parse(text)
+
+ def parse_text(self, text:'str'):
+ return self.parser.parse_text(text)
diff --git a/BetterMD/parse/markdown/parser.py b/BetterMD/parse/markdown/parser.py
new file mode 100644
index 0000000..d25a596
--- /dev/null
+++ b/BetterMD/parse/markdown/parser.py
@@ -0,0 +1,197 @@
+import re
+from ..typing import ELEMENT, TEXT
+import typing as t
+
+if t.TYPE_CHECKING:
+ from .typing import ELM_TYPE_W_END, ELM_TYPE_WO_END, ELM_TEXT
+ from . import Extension
+
+class MDParser:
+ extensions:'list[type[Extension]]' = []
+ top_level_tags:'dict[str, t.Union[ELM_TYPE_W_END, ELM_TYPE_WO_END]]' = {}
+ text_tags:'dict[str, ELM_TEXT]' = {}
+
+ @classmethod
+ def add_extension(cls, extension: 'type[Extension]'):
+ cls.extensions.append(extension)
+
+ @classmethod
+ def remove_extension(cls, extension: 'type[Extension]'):
+ cls.extensions.remove(extension)
+
+ @classmethod
+ def get_extension(cls, name: 'str') -> 't.Union[type[Extension], None]':
+ for extension in cls.extensions:
+ if extension.name == name:
+ return extension
+
+ def refresh_extensions(self):
+ self.top_level_tags = {}
+ self.text_tags = {}
+
+ for extension in self.extensions:
+ ext = extension(MDParser)
+ ext.init(self)
+ self.top_level_tags.update(ext.top_level_tags)
+ self.text_tags.update(ext.text_tags)
+ self.exts.append(ext)
+
+ def __init__(self):
+ self.exts:'list[Extension]' = []
+ self.reset()
+
+ def reset(self):
+ self.dom:'list[ELEMENT|TEXT]' = []
+ self.buffer = ""
+ self.end_func:'t.Optional[t.Callable[[], None]]' = None
+ self.dom_stack = []
+ self.head = []
+ self.block = None
+ self.parsing:'tuple[bool, list[str]]' = True, [] # bool - is parsing, list[str] - tags
+
+ for extension in self.exts:
+ extension.init(self)
+
+ @staticmethod
+ def create_element(name:'str', attrs:'dict[str, t.Union[str, bool, int, float]]'=None, children:'list[ELEMENT|TEXT]'=None) -> 'ELEMENT':
+ if children is None:
+ children = []
+
+ if attrs is None:
+ attrs = {}
+
+ return {
+ "type": "element",
+ "name": name,
+ "attributes": attrs,
+ "children": children
+ }
+
+ @staticmethod
+ def create_text(content:'str') -> 'TEXT':
+ return {
+ "type": "text",
+ "content": content,
+ "name": "text"
+ }
+
+ def end_block(self, parse=True):
+ if self.buffer and parse:
+ self.dom.append(self.parse_text(self.buffer))
+
+ if self.end_func is None:
+ return
+
+ self.dom.append(self.end_func())
+ self.end_func = None
+ self.block = None
+ self.parsing = True, []
+
+ def start_block(self, block, end_func=None):
+ self.end_block()
+ self.block = block
+ self.end_func = end_func
+
+ # Text
+
+ def handle_text(self, line: 'str'):
+ # Buffer text content for paragraph handling
+ if self.buffer:
+ self.buffer += '\n' + line
+ else:
+ self.buffer = line
+
+ def parse(self, markdown: 'str') -> 'list[ELEMENT]':
+ self.refresh_extensions()
+
+ for line in markdown.splitlines():
+ # Check for block-level elements
+ for tag, handler in self.top_level_tags.items():
+ if (not self.parsing[0]) and (tag not in self.parsing[1]):
+ continue
+ if re.search(handler["pattern"], line):
+ if handler["end"] is not None:
+ handler["handler"](line)
+ else:
+ self.dom.append(handler["handler"](line))
+ break
+
+ else:
+ # Regular text gets buffered for paragraph handling
+ self.handle_text(line)
+
+ # End any remaining block
+ self.end_block()
+
+ dom:'list[ELEMENT]' = []
+
+ for item in self.dom:
+ if isinstance(item, list):
+ dom.extend(item)
+ else:
+ dom.append(item)
+
+ return dom
+
+ def parse_text(self, text: 'str') -> 'list[ELEMENT | TEXT]':
+ self.refresh_extensions()
+ plain_text = ""
+ dom = []
+ i = 0
+
+ def handle(pattern, handler):
+ if re.match(pattern, text[i:]):
+ return True, *handler(text[i:])
+
+ return False, None, 0
+
+ while i < len(text):
+ for tag, handler in self.text_tags.items():
+ if not self.parsing[0] and tag not in self.parsing[1]:
+ continue
+
+ if isinstance(handler["pattern"], list):
+ b = False
+ for pattern in handler["pattern"]:
+ v, elm, l = handle(pattern, handler["handler"])
+ if v:
+ if plain_text:
+ dom.append(self.create_text(plain_text))
+ plain_text = ""
+
+ dom.append(elm)
+ i += l
+ b = True
+ break
+ if b:
+ break
+ else:
+ v, elm, l = handle(handler["pattern"], handler["handler"])
+ if v:
+ if plain_text:
+ dom.append(self.create_text(plain_text))
+ plain_text = ""
+
+ dom.append(elm)
+ i += l
+ break
+
+ else:
+ plain_text += text[i]
+
+ i += 1
+
+ if plain_text:
+ dom.append(self.create_text(plain_text))
+ plain_text = ""
+
+ return dom
+
+ def from_file(self, file):
+ with open(file, "r") as f:
+ self.parse(f.read())
+
+ head = self.create_element("head", children=self.head)
+ body = self.create_element("body", children=self.dom)
+
+ return self.create_element("html", children=[head, body])
\ No newline at end of file
diff --git a/BetterMD/parse/markdown/typing.py b/BetterMD/parse/markdown/typing.py
new file mode 100644
index 0000000..4680eee
--- /dev/null
+++ b/BetterMD/parse/markdown/typing.py
@@ -0,0 +1,58 @@
+import typing as t
+
+if t.TYPE_CHECKING:
+ from .parser import MDParser
+ from ..typing import ELEMENT, TEXT
+
+class ELM_TYPE_W_END(t.TypedDict):
+ pattern: 't.Union[str, list[str]]'
+ handler: 't.Callable[[str], None | t.NoReturn]'
+ end: 't.Callable[[], None]'
+
+
+class ELM_TYPE_WO_END(t.TypedDict):
+ pattern: 't.Union[str, list[str]]'
+ handler: 't.Callable[[str], ELEMENT]'
+ end: 'None'
+
+class ELM_TEXT(t.TypedDict):
+ pattern: 't.Union[str, list[str]]'
+ handler: 't.Callable[[str], tuple[TEXT | ELEMENT, int]]'
+
+class OL_LIST(t.TypedDict):
+ list: 't.Literal["ol"]'
+ input: 'bool'
+ checked: 'bool'
+ indent: 'int'
+ contents: 'str'
+ type: 't.Literal[")", "."]'
+ num: 'int'
+
+class UL_LIST(t.TypedDict):
+ list: 't.Literal["ul"]'
+ input: 'bool'
+ checked: 'bool'
+ indent: 'int'
+ contents: 'str'
+ type: 't.Literal["-", "*", "+"]'
+
+class LIST_ITEM(t.TypedDict):
+ data: 'OL_LIST | UL_LIST'
+ dataType: 't.Literal["item"]'
+
+class OL_TYPE(t.TypedDict):
+ value: 'list[LIST_TYPE | LIST_ITEM]'
+ parent: 'dict[str, LIST_TYPE]'
+ type: 't.Literal["ol"]'
+ key: 't.Literal["-", "*", "+", ")", "."]'
+ dataType: 't.Literal["list"]'
+ start: 'int'
+
+class UL_TYPE(t.TypedDict):
+ value: 'list[LIST_TYPE | LIST_ITEM]'
+ parent: 'dict[str, LIST_TYPE]'
+ type: 't.Literal["ul"]'
+ key: 't.Literal["-", "*", "+", ")", "."]'
+ dataType: 't.Literal["list"]'
+
+LIST_TYPE = t.Union[OL_TYPE, UL_TYPE]
\ No newline at end of file
diff --git a/BetterMD/parse/typing.py b/BetterMD/parse/typing.py
new file mode 100644
index 0000000..efb2282
--- /dev/null
+++ b/BetterMD/parse/typing.py
@@ -0,0 +1,18 @@
+import typing as t
+
+from ..typing import ATTRS
+
+class TEXT(t.TypedDict):
+ type: 't.Literal["text"]'
+ content: 'str'
+ name: 't.Literal["text"]'
+
+class ELEMENT(t.TypedDict):
+ type: 't.Literal["element"]'
+ name: 'str'
+ attributes: 'ATTRS'
+ children: 'list[t.Union[ELEMENT, TEXT]]'
+
+@t.runtime_checkable
+class Parser(t.Protocol):
+ def parse(self, html:'str') -> 'list[ELEMENT]': ...
\ No newline at end of file
diff --git a/BetterMD/rst/custom_rst.py b/BetterMD/rst/custom_rst.py
index ec2a085..d90fc5f 100644
--- a/BetterMD/rst/custom_rst.py
+++ b/BetterMD/rst/custom_rst.py
@@ -1,16 +1,18 @@
import typing as t
+from abc import ABC, abstractmethod
if t.TYPE_CHECKING:
from ..elements.symbol import Symbol
T = t.TypeVar("T", default='Symbol')
-class CustomRst(t.Generic[T]):
+class CustomRst(t.Generic[T], ABC):
prop = ""
rst: 'dict[str, str]' = {}
- def to_rst(self, inner: 'list[Symbol]', symbol:'T', parent:'Symbol') -> str: ...
+ @abstractmethod
+ def to_rst(self, inner: 'list[Symbol]', symbol:'T', parent:'Symbol', **kwargs) -> 'str': ...
- def prepare(self, inner:'list[Symbol]', symbol:'T', parent:'Symbol'): ...
+ def prepare(self, inner:'list[Symbol]', symbol:'T', parent:'Symbol', *args, **kwargs) -> 'list[Symbol]': ...
- def verify(self, text) -> bool: ...
\ No newline at end of file
+ def verify(self, text) -> 'bool': ...
\ No newline at end of file
diff --git a/BetterMD/typing.py b/BetterMD/typing.py
new file mode 100644
index 0000000..7a6f371
--- /dev/null
+++ b/BetterMD/typing.py
@@ -0,0 +1,11 @@
+import typing as t
+
+ATTR_TYPES = t.Union[str, bool, int, float, list, dict]
+
+ATTRS = t.Union[
+ t.TypedDict("ATTRS", {
+ "style": 'dict[str, ATTR_TYPES]',
+ "class": 'list[str]'
+}),
+ 'dict[str, ATTR_TYPES]'
+]
diff --git a/BetterMD/utils.py b/BetterMD/utils.py
new file mode 100644
index 0000000..cee3532
--- /dev/null
+++ b/BetterMD/utils.py
@@ -0,0 +1,48 @@
+import typing as t
+import sys
+
+if t.TYPE_CHECKING:
+ from .elements import Symbol
+
+T = t.TypeVar("T", default=t.Any)
+T2 = t.TypeVar("T2", default=t.Any)
+
+class List(list['Symbol'], t.Generic[T]):
+ def on_set(self, key, value): ...
+
+ def on_append(self, object: 'T'): ...
+
+ def append(self, object: 'T') -> 'None':
+ self.on_append(object)
+ return super().append(object)
+
+ def get(self, index, default:'T2'=None) -> 't.Union[T, T2]':
+ try:
+ return self[index]
+ except IndexError:
+ return default
+
+ def __setitem__(self, key, value):
+ self.on_set(key, value)
+ return super().__setitem__(key, value)
+
+ def __getitem__(self, item) -> 'T':
+ return super().__getitem__(item)
+
+ def __iter__(self) -> 't.Iterator[T]':
+ return super().__iter__()
+
+ def to_html(self):
+ return [elm.to_html() for elm in self]
+
+ def to_md(self):
+ return [elm.to_md() for elm in self]
+
+ def to_rst(self):
+ return [elm.to_rst() for elm in self]
+
+def set_recursion_limit(limit):
+ sys.setrecursionlimit(limit)
+
+def get_recursion_limit():
+ return sys.getrecursionlimit()
\ No newline at end of file
diff --git a/README.md b/README.md
index 12409e5..61f56a9 100644
--- a/README.md
+++ b/README.md
@@ -1,16 +1,75 @@
-# Better-md
+# BetterMD
## Insallation
```bash
pip install better-md
+
+# Extras
+
+pip install better-md[tables] # For pandas support
```
## Usage
+
+ HTML
+
+```python
+import BetterMD as md
+
+html = md.H1("Hello, world!").to_html()
+md = md.H1("Hello, world!")..to_md()
+rst = md.H1("Hello, world!").to_rst()
+```
+
+
+
+ Tables
+
```python
-from better_md import md
+import BetterMD as md
-html = md.H1("Hello, world!").prepare().to_html()
-md = md.H1("Hello, world!").prepare().to_md()
-```
\ No newline at end of file
+t = md.Table(
+ inner=[
+ md.THead(
+ inner=[
+ md.Tr(
+ inner=[
+ md.Th(inner=[md.Text("Header 1")], styles={"text-align":"left"}),
+ md.Th(inner=[md.Text("Header 2")], styles={"text-align":"center"}),
+ md.Th(inner=[md.Text("Header 3")], styles={"text-align":"right"}),
+ md.Th(inner=[md.Text("Header 4")])
+ ],
+ ),
+ ]
+ ),
+ md.TBody(
+ inner=[
+ md.Tr(
+ inner=[
+ md.Td(inner=[md.Text("Row 1 Cell 1")]),
+ md.Td(inner=[md.Text("Row 1 Cell 2")]),
+ md.Td(inner=[md.Text("Row 1 Cell 3")]),
+ md.Td(inner=[md.Text("Row 1 Cell 4")]),
+ ],
+ ),
+ md.Tr(
+ inner=[
+ md.Td(inner=[md.Text("Row 2 Cell 1")]),
+ md.Td(inner=[md.Text("Row 2 Cell 2")]),
+ md.Td(inner=[md.Text("Row 2 Cell 3")]),
+ md.Td(inner=[md.Text("Row 2 Cell 4")]),
+ ]
+ )
+ ]
+ )
+ ]
+)
+
+t.to_md()
+t.to_rst()
+t.to_html()
+t.to_pandas() # Requires `tables` extra
+```
+
\ No newline at end of file
diff --git a/TODO.md b/TODO.md
index 83b212e..ff7aebb 100644
--- a/TODO.md
+++ b/TODO.md
@@ -3,10 +3,19 @@
## Todo
- [x] Add basic architecture
-- [x] Add HTML elelemts
-- [x] Publish to Pypi
+- [x] Add HTML elements
+- [x] Publish to PYPI
- [x] Add RST support
- [ ] Add parsing support
-- [ ] Add innerline support
-- [ ] Add escape chars (\\)
+ - [x] Add HTML parsing
+ - [x] Add MD parsing
+ - [x] Add inline support
+ - [x] Add escape chars (\\)
+ - [ ] Add RST parsing
- [ ] Add other languages
+
+
+## Inportant
+
+Fix table.py
+Delete this section
\ No newline at end of file
diff --git a/setup.py b/setup.py
index 989e1dc..993a20e 100644
--- a/setup.py
+++ b/setup.py
@@ -1,6 +1,6 @@
from setuptools import setup, find_packages
-VERSION = "0.0.1"
+VERSION = "0.2.3"
DESCRIPTION = "A better markdown library"
setup(
@@ -10,7 +10,11 @@
description=DESCRIPTION,
long_description=open("README.md").read(),
long_description_content_type="text/markdown",
- packages=find_packages(),
+ packages=find_packages(exclude="tests"),
+ install_requires=[],
+ extras_require={
+ "tables": ["pandas==2.2.3"]
+ },
keywords=['python', 'better markdown', 'markdown'],
classifiers= [
"Development Status :: 3 - Alpha",
@@ -20,5 +24,5 @@
"Operating System :: Microsoft :: Windows",
"Operating System :: POSIX :: Linux",
],
- url="https://github.com/Betters-Markdown/better_md"
-)
\ No newline at end of file
+ url="https://github.com/Better-MD/better-md"
+)
diff --git a/src/better_md/__init__.py b/src/better_md/__init__.py
new file mode 100644
index 0000000..bdae74d
--- /dev/null
+++ b/src/better_md/__init__.py
@@ -0,0 +1,2 @@
+def main() -> None:
+ print("Hello from better-md!")
diff --git a/t.md b/t.md
deleted file mode 100644
index e69de29..0000000
diff --git a/tests/test.py b/tests/test.py
index 43fb5da..3941def 100644
--- a/tests/test.py
+++ b/tests/test.py
@@ -1,4 +1,6 @@
-from BetterMD import H1, H2, Text, Div, LI, OL, UL, A, Strong, Table, Tr, Td, Th, Blockquote, Em, Input, CustomRst, CustomHTML, CustomMarkdown
+from BetterMD import H1, H2, Text, Div, LI, OL, UL, A, B, Table, Tr, Td, Th, THead, TBody, Blockquote, I, Input, CustomRst, CustomHTML, CustomMarkdown, enable_debug_mode
+
+# enable_debug_mode()
print(H1(inner=[Text("Hi")]).to_html())
print(H1(inner=[Text("Hi")]).to_md())
@@ -34,42 +36,64 @@
)
# Bold text
-print(Strong(inner=[Text("Bold text")]).prepare(None).to_md()) # **Bold text**
+print(B(inner=[Text("Bold text")]).prepare(None).to_md()) # **Bold text**
# Table example
-print("RST",
- Table(
+t=Table(
inner=[
- Tr(
+ THead(
inner=[
- Th(inner=[Text("Header 1")]),
- Th(inner=[Text("Header 2")])
- ],
- is_header=True
+ Tr(
+ inner=[
+ Th(inner=[Text("Header 1")], styles={"text-align":"left"}),
+ Th(inner=[Text("Header 2 WIDER")], styles={"text-align":"center"}),
+ Th(inner=[Text("Header 3")], styles={"text-align":"right"}),
+ Th(inner=[Text("SMALL")]),
+ Th()
+ ],
+ ),
+ ]
),
- Tr(
+ TBody(
inner=[
- Td(inner=[Text("Cell 1")]),
- Td(inner=[Text("Cell 2")])
+ Tr(
+ inner=[
+ Td(inner=[Text("Row 1 Cell 1 EXTRA LONG")]),
+ Td(inner=[Text("Row 1 Cell 2")]),
+ Td(inner=[Text("Row 1 Cell 3")]),
+ Td(inner=[Text("SMALL")]),
+ Td()
+ ],
+ ),
+ Tr(
+ inner=[
+ Td(inner=[Text("Row 2 Cell 1")]),
+ Td(inner=[Text("Row 2 Cell 2")]),
+ Td(inner=[Text("Row 2 Cell 3 EXTRA LONG")]),
+ Td(inner=[Text("SMALL")]),
+ Td(inner=[Text("2")])
+ ]
+ )
]
)
]
- ).prepare(None).to_rst(), sep="\n"
+ ).prepare()
+
+print(
+ "\n",
+ t.to_rst(),
+ sep=""
)
-"""
-|Header 1|Header 2|
-|---|---|
-|Cell 1|Cell 2|
-"""
+
# Blockquote with formatting
print(
Blockquote(
inner=[
Text("A quote with "),
- Strong(inner=[Text("bold")]),
+ B(inner=[Text("bold")]),
Text(" and "),
- Em(inner=[Text("italic")]),
+ I(inner=[Text("italic")]),
Text(" text.")
]
).prepare(None).to_md()