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"![{alt}]({symbol.get_prop('src')})" - -class HTML(CustomHTML): - def to_html(self, inner, symbol, parent): - return f"{symbol.get_prop('alt'," + return f"![{alt}]({symbol.get_prop('src')})" 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}" - + 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}" + 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"" + 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()