Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 65 additions & 7 deletions snakemd/document.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,13 @@
Raw,
Table,
)
from .templates import TableOfContents, Template
from .templates import (
Alert,
CSVTable,
Template,
Checklist,
TableOfContents,
)

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -263,15 +269,15 @@ def add_unordered_list(self, items: Iterable[str]) -> MDList:
logger.info("Added unordered list to document: %r", md_list)
return md_list

def add_checklist(self, items: Iterable[str]) -> MDList:
def add_checklist(self, items: Iterable[str]) -> Checklist:
"""
A convenience method which adds a checklist to the document.

.. doctest:: document

>>> doc = snakemd.new_doc()
>>> doc.add_checklist(["Okabe", "Mayuri", "Kurisu"])
MDList(items=[...], ordered=False, checked=False)
Checklist(items=[...], checked=False)
>>> print(doc)
- [ ] Okabe
- [ ] Mayuri
Expand All @@ -282,10 +288,10 @@ def add_checklist(self, items: Iterable[str]) -> MDList:
:return:
the :class:`MDList` added to this Document
"""
md_checklist = MDList(items, checked=False)
self._elements.append(md_checklist)
logger.info("Added checklist to document: %r", md_checklist)
return md_checklist
checklist = Checklist(items, checked=False)
self._elements.append(checklist)
logger.info("Added checklist to document: %r", checklist)
return checklist

def add_table(
self,
Expand Down Expand Up @@ -330,6 +336,33 @@ def add_table(
logger.info("Added table to document: %r", table)
return table

def add_table_from_csv(self, path: os.PathLike) -> CSVTable:
"""
A convenience method which adds a table from a CSV file to
the document:

.. doctest:: document

>>> doc = snakemd.new_doc()
>>> doc.add_table_from_csv("../tests/resources/python-support.csv")
Table(header=[...], body=[...], align=None, indent=0)
>>> print(doc) # doctest: +NORMALIZE_WHITESPACE
| Python | 3.11 | 3.10 | 3.9 | 3.8 |
| ------------------- | ---- | ---- | --- | --- |
| SnakeMD >= 2.0 | Yes | Yes | Yes | Yes |
| SnakeMD 0.12 - 0.15 | Yes | Yes | Yes | Yes |
| SnakeMD < 0.12 | | Yes | Yes | Yes |

:param os.PathLike path:
a path to a CSV file
:return:
the :class:`Table` added to this Document
"""
table = CSVTable(path)
self._elements.append(table)
logger.info("Added table to document: %r", table)
return table

def add_code(self, code: str, lang: str = "generic") -> Code:
"""
A convenience method which adds a code block to the document:
Expand Down Expand Up @@ -433,6 +466,31 @@ def add_table_of_contents(self, levels: range = range(2, 3)) -> TableOfContents:
logger.info("Added table of contents to document: %r", toc)
return toc

def add_alert(self, message: str, kind: Alert.Kind = Alert.Kind.NOTE) -> Alert:
"""
A convenience method which adds an alert to the document:

.. doctest:: document

>>> doc = snakemd.new_doc()
>>> doc.add_alert("Please subscribe")
Quote(content=[Raw(text='[!NOTE]'), Raw(text='Please subscribe')])
>>> print(doc)
> [!NOTE]
> Please subscribe

:param str message:
a message that you want to stand out in your document
:param Kind kind:
the kind of alert; see :class:`snakemd.Alerts.Kind` for details
:return:
the :class:`Alert` added to this Document
"""
alert = Alert(message, kind)
self._elements.append(alert)
logger.info("Added alert to document: %r", alert)
return alert

def scramble(self) -> None:
"""
A silly method which mixes all of the blocks in this document in
Expand Down
55 changes: 28 additions & 27 deletions snakemd/elements.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,8 @@ class Inline(Element):
the line break state of the inline text

- defaults to :code:`False`
- set to :code:`True` to add a line break to the end of the element (i.e., `<br>`)
- set to :code:`True` to add a line break to the
end of the element (i.e., `<br>`)
"""

def __init__(
Expand Down Expand Up @@ -385,7 +386,7 @@ def uncode(self) -> Inline:
"""
self._code = False
return self

def breakline(self) -> Inline:
"""
Adds a linebreak to self.
Expand All @@ -401,7 +402,7 @@ def breakline(self) -> Inline:
"""
self._linebreak = True
return self

def unbreakline(self) -> Inline:
"""
Removes linebreak from self.
Expand Down Expand Up @@ -481,7 +482,7 @@ def reset(self) -> Inline:
self._bold = False
self._strikethrough = False
return self

def _apply_styles_from(self, text: Inline) -> Inline:
"""
A helper method that applies text styling to self from another
Expand Down Expand Up @@ -862,8 +863,9 @@ def __init__(
checked if checked is None or isinstance(checked, bool) else list(checked)
)
self._space = ""
if isinstance(self._checked, list) and MDList._top_level_count(self._items) != len(
self._checked
if (
isinstance(self._checked, list)
and MDList._top_level_count(self._items) != len(self._checked)
):
raise ValueError(
"Number of top-level elements in checklist does not "
Expand Down Expand Up @@ -1117,28 +1119,27 @@ def _replace_any(self, target: str, text: Inline, count: int = -1) -> Paragraph:
if not inline_text.is_text() or target not in inline_text.get_text():
content.append(inline_text)
continue
# Split the inline element into pieces and reapply styles
else:
# Redistributes styles
items = [
Inline(item)._apply_styles_from(inline_text)
for item in inline_text.get_text().split(target, count)
]

# Adds text back in with appropriate style information
for item in items:
content.append(item)
content.append(text._apply_styles_from(inline_text))

# Split the inline element into pieces
items = [
Inline(item)._apply_styles_from(inline_text)
for item in inline_text.get_text().split(target, count)
]

# Adds text back in with appropriate style information
for item in items:
content.append(item)
content.append(text._apply_styles_from(inline_text))
content.pop()

# Trim empty strings from edges
if content[-1].get_text() == "":
content.pop()

# Trim empty strings from edges
if content[-1].get_text() == "":
content.pop()
if content[0].get_text() == "":
content = content[1:]

# Remove line breaks that may have been distributed by applying styles
content[:-1] = map(lambda item: item.unbreakline(), content[:-1])
if content[0].get_text() == "":
content = content[1:]

# Remove line breaks that may have been distributed by applying styles
content[:-1] = map(lambda item: item.unbreakline(), content[:-1])

self._content = content
return self
Expand Down
18 changes: 12 additions & 6 deletions snakemd/templates.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,9 @@ def load(self, elements: list[Element]) -> None:
self._elements = elements


class Alerts(Template):
class Alert(Template):
"""
Alerts are a wrapper of the Quote object to provide
Alert is a wrapper of the Quote object to provide
support for the alerts Markdown extension. While
quotes can be nested in each other, alerts cannot.

Expand All @@ -77,6 +77,11 @@ class Alerts(Template):
"""

class Kind(Enum):
"""
Kind is an enum representing the different
kinds of alerts that you might place in a
document.
"""
NOTE = auto()
TIP = auto()
IMPORTANT = auto()
Expand All @@ -85,8 +90,8 @@ class Kind(Enum):

def __init__(
self,
kind: Kind,
message: str | Iterable[str | Inline | Block]
message: str | Iterable[str | Inline | Block],
kind: Kind
) -> None:
super().__init__()
self._kind = kind
Expand Down Expand Up @@ -154,8 +159,9 @@ def __init__(
checked, bool) else list(checked)
)
self._space = ""
if isinstance(self._checked, list) and MDList._top_level_count(self._items) != len(
self._checked
if (
isinstance(self._checked, list)
and MDList._top_level_count(self._items) != len(self._checked)
):
raise ValueError(
"Number of top-level elements in checklist does not "
Expand Down
26 changes: 13 additions & 13 deletions tests/templates/test_alerts.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,26 @@
from snakemd.elements import Inline
from snakemd.templates import Alerts
from snakemd.templates import Alert

def test_alerts_note():
alert = Alerts(Alerts.Kind.NOTE, "Hello, World!")
def test_alert_note():
alert = Alert("Hello, World!", Alert.Kind.NOTE)
assert str(alert) == "> [!NOTE]\n> Hello, World!"

def test_alerts_tip():
alert = Alerts(Alerts.Kind.TIP, "Hello, World!")
def test_alert_tip():
alert = Alert("Hello, World!", Alert.Kind.TIP)
assert str(alert) == "> [!TIP]\n> Hello, World!"

def test_alerts_important():
alert = Alerts(Alerts.Kind.IMPORTANT, "Hello, World!")
def test_alert_important():
alert = Alert("Hello, World!", Alert.Kind.IMPORTANT)
assert str(alert) == "> [!IMPORTANT]\n> Hello, World!"

def test_alerts_warning():
alert = Alerts(Alerts.Kind.WARNING, "Hello, World!")
def test_alert_warning():
alert = Alert("Hello, World!", Alert.Kind.WARNING)
assert str(alert) == "> [!WARNING]\n> Hello, World!"

def test_alerts_caution():
alert = Alerts(Alerts.Kind.CAUTION, "Hello, World!")
def test_alert_caution():
alert = Alert("Hello, World!", Alert.Kind.CAUTION, )
assert str(alert) == "> [!CAUTION]\n> Hello, World!"

def test_alerts_inline():
alert = Alerts(Alerts.Kind.NOTE, Inline("Hello, World!", italics=True))
def test_alert_inline():
alert = Alert(Inline("Hello, World!", italics=True), Alert.Kind.NOTE)
assert str(alert) == "> [!NOTE]\n> _Hello, World!_"