diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000..83e659c
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,69 @@
+name: "Release"
+
+on:
+ workflow_dispatch:
+ inputs:
+ tag:
+ description: "The version to tag"
+ type: string
+ sha:
+ description: "The full sha of the commit to be release. If omitted, the most recent commit on the default branch will be used."
+ default: ""
+ type: string
+jobs:
+ tag-release:
+ name: Tag Release
+ runs-on: ubuntu-latest
+ if: ${{ inputs.tag }}
+ needs: pypi-publish
+ permissions:
+ contents: write
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ ref: ${{ inputs.sha }}
+ - name: Git tag
+ run: |
+ git config user.name "Github Actions"
+ git config user.email "actions@github.com"
+ git tag -m "${{ inputs.tag }}" "${{ inputs.tag }}"
+ git push --tags
+
+ pypi-publish:
+ name: upload release to PyPI
+ runs-on: ubuntu-latest
+ permissions:
+ # IMPORTANT: this permission is mandatory for trusted publishing
+ id-token: write
+
+ steps:
+ # retrieve your distributions here
+ - uses: actions/checkout@v4
+ with:
+ ref: ${{ inputs.sha }}
+
+ - name: Install rye
+ uses: eifinger/setup-rye@v1
+ with:
+ enable-cache: true
+ - name: Install Dependencies
+ run: rye sync
+ - name: Build Project
+ run: rye build
+ - name: Publish package distributions to PyPI
+ uses: pypa/gh-action-pypi-publish@release/v1
+ with:
+ skip_existing: true
+ packages_dir: dist
+ verbose: true
+
+ publish-release:
+ name: Publish to Github
+ runs-on: ubuntu-latest
+ needs: tag-release
+ steps:
+ - uses: softprops/action-gh-release@v1
+ with:
+ draft: true
+ tag_name: ${{ inputs.tag }}
+
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index bdc4cb7..2446a49 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -8,9 +8,32 @@ on:
- opened
- synchronize
+
jobs:
+ changes:
+ runs-on: ubuntu-latest
+ permissions:
+ pull-requests: read
+ outputs:
+ changed: ${{ steps.filter.outputs.changed }}
+ steps:
+ - uses: actions/checkout@v3
+ - uses: dorny/paths-filter@v2
+ id: filter
+ with:
+ filters: |
+ changed:
+ - 'tests/**'
+ - 'src/**'
+ - name: Dump GitHub context
+ env:
+ GITHUB_CONTEXT: ${{ toJson(github) }}
+ run: echo "$GITHUB_CONTEXT"
+
lint:
runs-on: ubuntu-latest
+ needs: changes
+ if: ${{needs.changes.outputs.changed == 'true'}}
steps:
- uses: actions/checkout@v3
- name: Install rye
@@ -26,10 +49,13 @@ jobs:
tests:
runs-on: ubuntu-latest
+ needs: [changes, lint]
+ if: ${{needs.changes.outputs.changed == 'true'}}
strategy:
matrix:
python-version: [3.8, 3.9, "3.10","3.11","3.12"]
steps:
+
- uses: actions/checkout@v3
- name: Install rye
uses: eifinger/setup-rye@v1
diff --git a/.gitignore b/.gitignore
index e58fa36..9efe636 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,3 +2,4 @@
**/__pycache__/
tests/.coverage
.coverage
+dist/
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..5e37494
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,19 @@
+Copyright (c) 2023 Tyler Baur
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
index 69c4add..c5cd6f2 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,77 @@
# PyMdown Extensions Blocks
+[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
+![PyPI - Version](https://img.shields.io/pypi/v/pymdownx-blocks)
+![PyPI - Python Version](https://img.shields.io/pypi/pyversions/pymdownx-blocks)
+![Tests](https://img.shields.io/github/actions/workflow/status/TillerBurr/pymdownx-blocks/tests)
+
+
+
These are a collection of blocks for the [PyMdown Extensions](https://facelessuser.github.io/pymdown-extensions) that I find useful.
This project is not affiliated with the PyMdown Extensions project and is currently in a very early stage. Currently, there is only one block: `DirTree`.
+## Installation
+
+```bash
+pip install pymdownx-blocks
+```
+
+## Usage
+This can be used in MkDocs or by itself. To use in a Python file, we use the following:
+
+```python
+import markdown
+
+yaml_str=...
+md=markdown.Markdown(extensions=['pymdownx_blocks.dirtree'])
+```
+
+To use in MkDocs, register the extension.
+
+```yaml
+...
+markdown_extensions:
+...
+- pymdownx_blocks.dirtree
+...
+```
+
+In a markdown file,
+```
+///dirtree
+
+root:
+- subdir:
+ - File
+- another subdir:
+ - anotherfile.txt
+ - file.csv
+///
+```
+
+
+When rendered, this will produce the following tree
+
+
+
Directory Structure
+
+root
+├── subdir
+│ └── File
+└── another subdir
+ ├── anotherfile.txt
+ └── file.csv
+
+
+
+
+## Contributing
+
+More blocks are always welcome! This project uses [rye](https:rye-up.com) for dependency
+management.
+
+1. Fork the repository
+2. Create a branch with the name of the block
+3. Implement the block
+4. Create a pull request.
diff --git a/src/pymdownx_blocks/dirtree.py b/src/pymdownx_blocks/dirtree.py
index f775cd7..ea601bb 100644
--- a/src/pymdownx_blocks/dirtree.py
+++ b/src/pymdownx_blocks/dirtree.py
@@ -10,11 +10,11 @@
class InvalidYAMLError(BaseException):
- """Raised when YAML is invalid"""
+ """YAML Cannot be parsed."""
class InvalidTreeError(BaseException):
- """Raised when the YAML is valid, but it not a valid tree"""
+ """YAML is valid, but not a valid directory tree."""
PIPE = "│"
@@ -27,13 +27,35 @@ class InvalidTreeError(BaseException):
def sorter(x: Union[TreeNode, str]) -> tuple[bool, str]:
+ """
+
+ Args:
+ x (Union[TreeNode, str]): An element of a TreeNode
+
+ Returns:
+ (tuple[bool, str]): A tuple containing a bool that is `True` if x is a `str`,
+ and a str containing either `x` or the first (and only) key of `x`
+
+ """
_type = isinstance(x, str)
_value = x if isinstance(x, str) else next(iter(x.keys()))
return _type, _value
class DirTree:
- def __init__(self, in_: str):
+ """
+ Create a directory tree from a YAML string.
+ """
+
+ def __init__(self, in_: str) -> None:
+ """Initialize the tree object.
+
+ Args:
+ in_ (str): YAML string
+
+ Raises:
+ InvalidTreeError: Not a valid tree.
+ """
try:
self.tree = yaml.safe_load(in_)
if not isinstance(self.tree, dict):
@@ -43,7 +65,7 @@ def __init__(self, in_: str):
except yaml.error.YAMLError:
raise InvalidYAMLError
- def build_output(
+ def build(
self,
tree: TreeNode,
current_index: int = 0,
@@ -51,7 +73,22 @@ def build_output(
parent_siblings: int = 0,
item_sep: str = "",
is_root: bool = True,
- ):
+ ) -> str:
+ """
+ Build the output. The final result is a string containing the tree.
+
+ Args:
+ tree (TreeNode): A node in the tree. It is a dict containing values that are
+ a list of strings or dictionaries.
+ current_index (int): Current index of the item in the sequence.
+ prefix (str): String that is prepended to each line.
+ parent_siblings (int): Number of siblings the parent has.
+ item_sep (str): An Elbow or Tee. Used between lines, next to items.
+ is_root (bool): Declares the root of the tree.
+
+ Returns:
+ A string containing the parsed tree.
+ """
_tree = ""
directory = next(iter(tree.keys()))
contents = tree.get(directory, [])
@@ -79,7 +116,7 @@ def build_output(
new_prefix = prefix + curr_prefix
if isinstance(element, dict):
# A dict is a subtree, build the subtree
- _tree += self.build_output(
+ _tree += self.build(
element,
item_index,
parent_siblings=num_siblings,
@@ -89,19 +126,38 @@ def build_output(
)
else:
_tree += f"{new_prefix}{item_sep}{element}\n"
+ if is_root:
+ return _tree.rstrip()
return _tree
- def build(self) -> str:
- final_tree = self.build_output(self.tree).rstrip()
- return final_tree
+ def build_tree(self) -> str:
+ """Build the tree, without needing to pass in `self.tree`.
+
+ Returns:
+ (str): The string containing the directory tree.
+
+ """
+ return self.build(self.tree)
class DirTreeBlock(Block):
+ """Block Extension for the DirTree"""
+
NAME = "dirtree"
ARGUMENT = None
OPTIONS = {"type": ["", type_html_identifier]}
def on_create(self, parent: etree.Element) -> etree.Element:
+ """
+
+ Args:
+ parent (xml.etree.ElementTree.Element): The parent element in the XML tree.
+
+ Returns:
+ (xml.etree.ElementTree.Element): A div container with given classes and
+ title text.
+
+ """
classes = ["admonition"]
self_type = self.options["type"]
if self_type:
@@ -115,21 +171,41 @@ def on_create(self, parent: etree.Element) -> etree.Element:
return el
def on_end(self, block: etree.Element) -> None:
+ """Inserts the DirTree into the container
+
+ Args:
+ block (xml.etree.ElementTree.Element): The block/container created in
+ `on_create`
+ """
yaml_content = block.find("p[2]")
yaml_tree = block.findtext("p[2]")
if yaml_tree and yaml_content is not None:
block.remove(yaml_content)
tree = DirTree(yaml_tree)
dt = etree.SubElement(block, "pre")
- dt.text = tree.build()
+ dt.text = tree.build_tree()
class DirTreeExtension(BlocksExtension):
+ """Extension for the DirTree"""
+
def extendMarkdownBlocks(
self, md: markdown.core.Markdown, block_mgr: BlocksProcessor
) -> None:
+ """Register the `DirTreeBlock` for pymdownx.blocks
+
+ Args:
+ md (markdown.core.Markdown): The markdown parser
+ block_mgr (BlocksProcessor): The Generic Block Processor
+ """
block_mgr.register(DirTreeBlock, self.getConfigs())
def makeExtension(*args, **kwargs) -> DirTreeExtension:
+ """Register the extension with MkDocs.
+
+ Returns:
+ (DirTreeExtension): The Directory Tree extension.
+
+ """
return DirTreeExtension(*args, **kwargs)
diff --git a/tests/test_dirtree.py b/tests/test_dirtree.py
index 67e0e6d..d7d12bd 100644
--- a/tests/test_dirtree.py
+++ b/tests/test_dirtree.py
@@ -1,8 +1,8 @@
-from .utils import dedent, dedent_and_replace
import pytest
import yaml
from pymdownx_blocks.dirtree import DirTree, InvalidTreeError, InvalidYAMLError
+from tests.utils import dedent, dedent_and_replace
root_dir_files = """
root_dir/:
- file1
@@ -168,7 +168,7 @@ def test_invalid_yaml():
@pytest.mark.parametrize("class_type", ["note", "warning", "tip", "danger", None])
-@pytest.mark.parametrize("title", ["A Title", "Another Title", None])
+@pytest.mark.parametrize("title", ["A Title", "AnotherTitle",None])
def test_title(markdown_fixture, class_type, title):
md = markdown_fixture(
["pymdownx_blocks.dirtree"], extension_config={"pymdownx_blocks.dirtree": []}