Skip to content

Commit

Permalink
Add ability to generate a markdown index
Browse files Browse the repository at this point in the history
  • Loading branch information
robtaylor committed Oct 7, 2024
1 parent 7865edf commit 0014cd6
Show file tree
Hide file tree
Showing 7 changed files with 182 additions and 9 deletions.
7 changes: 6 additions & 1 deletion .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ omit =
*/tests/*
*/__main__.py

dynamic_context = test_function

[report]

omit =
Expand All @@ -20,4 +22,7 @@ exclude_lines =
raise NotImplementedError
except DistributionNotFound

skip_covered = true
skip_covered = false

[html]
show_contexts = True
3 changes: 3 additions & 0 deletions doorstop/cli/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -561,6 +561,9 @@ def run_publish(args, cwd, error, catch=True):
if args.width:
kwargs["width"] = args.width

if args.index:
kwargs["index"] = True

# Write to output file(s)
if args.path:
path = os.path.abspath(os.path.join(cwd, args.path))
Expand Down
9 changes: 8 additions & 1 deletion doorstop/cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,9 @@ def main(args=None): # pylint: disable=R0915

# Build main parser
parser = argparse.ArgumentParser(
prog=CLI, description=DESCRIPTION, **shared # type: ignore
prog=CLI,
description=DESCRIPTION,
**shared, # type: ignore
)
parser.add_argument(
"-F",
Expand Down Expand Up @@ -536,6 +538,11 @@ def _publish(subs, shared):
help="do not include levels on heading and non-heading or non-heading items",
)
sub.add_argument("--template", help="template file", default=None)
sub.add_argument(
"--index",
help="Generate top level index (when producing markdown).",
action="store_true",
)


if __name__ == "__main__":
Expand Down
28 changes: 28 additions & 0 deletions doorstop/cli/tests/test_all.py
Original file line number Diff line number Diff line change
Expand Up @@ -810,10 +810,38 @@ def test_publish_tree_text(self):
self.assertTrue(os.path.isdir(path))
self.assertFalse(os.path.isfile(os.path.join(path, "index.html")))

def test_publish_tree_md(self):
"""Verify 'doorstop publish' can create a Markdown directory."""
path = os.path.join(self.temp, "all")
self.assertIs(None, main(["publish", "all", path, "--markdown", "--index"]))
self.assertTrue(os.path.isdir(path))
self.assertTrue(os.path.isfile(os.path.join(path, "index.md")))

def test_publish_tree_no_path(self):
"""Verify 'doorstop publish' returns an error with no path."""
self.assertRaises(SystemExit, main, ["publish", "all"])

def test_publish_tree_markdown_with_index(self):
"""Verify 'doorstop publish' can create Markdown output for a tree,
with an index."""
path = os.path.join(self.temp, "all")
self.assertIs(None, main(["publish", "all", path, "--markdown", "--index"]))
self.assertTrue(os.path.isdir(path))
self.assertTrue(os.path.isfile(os.path.join(path, "index.md")))

def test_publish_markdown_tree_no_path(self):
"""Verify 'doorstop publish' returns an error with no path."""
self.assertRaises(
SystemExit,
main,
[
"publish",
"-m",
"--index",
"all",
],
)


class TestPublishCommand(TempTestCase):
"""Tests 'doorstop publish' options toc and template"""
Expand Down
71 changes: 68 additions & 3 deletions doorstop/core/publishers/markdown.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,85 @@

"""Functions to publish documents and items."""

import os
from re import sub

from doorstop import common, settings
from doorstop.core.publishers.base import BasePublisher, format_level
from doorstop.core.publishers.base import (
BasePublisher,
extract_prefix,
format_level,
get_document_attributes,
)
from doorstop.core.types import is_item, iter_items

log = common.logger(__name__)
INDEX = "index.md"


class MarkdownPublisher(BasePublisher):
"""Markdown publisher."""

def create_index(self, directory, index=None, extensions=(".md",), tree=None):
"""No index for Markdown."""
def create_index(self, directory, index=INDEX, extensions=(".md",), tree=None):
"""Create an markdown index of all files in a directory.
:param directory: directory for index
:param index: filename for index
:param extensions: file extensions to include
:param tree: optional tree to determine index structure
"""
# Get paths for the index index
filenames = []
for filename in os.listdir(directory):
if filename.endswith(extensions) and filename != INDEX:
filenames.append(os.path.join(filename))

# Create the index
if filenames:
path = os.path.join(directory, index)
log.info("creating an {}...".format(index))
lines = self.lines_index(sorted(filenames), tree=tree)
common.write_text(" # Requirements index", path)
common.write_text("\n".join(lines), path)
else:
log.warning("no files for {}".format(index))

def _index_tree(self, tree, depth):
"""Recursively generate markdown index.
:param tree: optional tree to determine index structure
:param depth: depth recursed into tree
"""

depth = depth + 1

title = get_document_attributes(tree.document)["title"]
prefix = extract_prefix(tree.document)
filename = f"{prefix}.md"

# Tree structure
yield " " * (depth * 2 - 1) + f"* [{prefix}]({filename}) - {title}"
# yield self.table_of_contents(linkify=True, obj=tree.document, depth=depth, heading=False)
for child in tree.children:
yield from self._index_tree(tree=child, depth=depth)

def lines_index(self, filenames, tree=None):
"""Yield lines of Markdown for index.md.
:param filenames: list of filenames to add to the index
:param tree: optional tree to determine index structure
"""
if tree:
yield from self._index_tree(tree, depth=0)

# Additional files
if filenames:
yield ""
yield "### Published Documents:"
for filename in filenames:
name = os.path.splitext(filename)[0]
yield " * [{n}]({f})".format(f=filename, n=name)

def create_matrix(self, directory):
"""No traceability matrix for Markdown."""
Expand Down
53 changes: 51 additions & 2 deletions doorstop/core/publishers/tests/test_publisher_markdown.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@
# pylint: disable=unused-argument,protected-access

import os
import stat
import unittest
from secrets import token_hex
from shutil import rmtree
from unittest.mock import Mock, patch
from unittest.mock import MagicMock, Mock, patch

from doorstop.core import publisher
from doorstop.core.publishers.tests.helpers import YAML_CUSTOM_ATTRIBUTES, getLines
from doorstop.core.tests import (
EMPTY,
FILES,
ROOT,
MockDataMixIn,
Expand All @@ -22,6 +22,7 @@
MockItemAndVCS,
)
from doorstop.core.tests.helpers import on_error_with_retry
from doorstop.core.types import UID


class TestModule(MockDataMixIn, unittest.TestCase):
Expand Down Expand Up @@ -263,3 +264,51 @@ def test_toc(self):
md_publisher = publisher.check(".md", self.document)
toc = md_publisher.table_of_contents(linkify=True, obj=self.document)
self.assertEqual(expected, toc)

def test_index(self):
"""Verify an Markdown index can be created."""
# Arrange
path = os.path.join(FILES, "index.md")
md_publisher = publisher.check(".md")
# Act
md_publisher.create_index(FILES)
# Assert
self.assertTrue(os.path.isfile(path))

def test_index_no_files(self):
"""Verify an Markdown index is only created when files exist."""
path = os.path.join(EMPTY, "index.md")
md_publisher = publisher.check(".md")
# Act
md_publisher.create_index(EMPTY)
# Assert
self.assertFalse(os.path.isfile(path))

def test_index_tree(self):
"""Verify an Markdown index can be created with a tree."""
path = os.path.join(FILES, "index2.md")
mock_tree = MagicMock()
mock_tree.documents = []
for prefix in ("SYS", "HLR", "LLR", "HLT", "LLT"):
mock_document = MagicMock()
mock_document.prefix = prefix
mock_tree.documents.append(mock_document)
mock_tree.draw = lambda: "(mock tree structure)"
mock_item = Mock()
mock_item.uid = "KNOWN-001"
mock_item.document = Mock()
mock_item.document.prefix = "KNOWN"
mock_item.header = None
mock_item_unknown = Mock(spec=["uid"])
mock_item_unknown.uid = "UNKNOWN-002"
mock_trace = [
(None, mock_item, None, None, None),
(None, None, None, mock_item_unknown, None),
(None, None, None, None, None),
]
mock_tree.get_traceability = lambda: mock_trace
md_publisher = publisher.check(".md")
# Act
md_publisher.create_index(FILES, index="index2.md", tree=mock_tree)
# Assert
self.assertTrue(os.path.isfile(path))
20 changes: 18 additions & 2 deletions doorstop/core/tests/test_publisher.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,22 @@ def test_index_none_for_md(self):
# Assert
self.assertEqual(result, False)

def test_index_true_md(self):
"""Verify that index = true forces true."""
tmp_publisher = publisher.check(".md", self.mock_tree)
tmp_publisher.setup(None, True, None)
do_index = tmp_publisher.getIndex()
# Assert
self.assertEqual(do_index, True)

def test_index_false_md(self):
"""Verify that index = false forces false."""
tmp_publisher = publisher.check(".md", self.mock_tree)
tmp_publisher.setup(None, False, None)
do_index = tmp_publisher.getIndex()
# Assert
self.assertEqual(do_index, False)

def test_index_none_for_txt(self):
"""Verify that index = None works correctly."""
tmp_publisher = publisher.check(".txt", self.mock_tree)
Expand All @@ -136,15 +152,15 @@ def test_index_none_for_txt(self):
# Assert
self.assertEqual(result, False)

def test_index_true(self):
def test_index_true_html(self):
"""Verify that index = true forces true."""
tmp_publisher = publisher.check(".html", self.mock_tree)
tmp_publisher.setup(None, True, None)
do_index = tmp_publisher.getIndex()
# Assert
self.assertEqual(do_index, True)

def test_index_false(self):
def test_index_false_html(self):
"""Verify that index = false forces false."""
tmp_publisher = publisher.check(".html", self.mock_tree)
tmp_publisher.setup(None, False, None)
Expand Down

0 comments on commit 0014cd6

Please sign in to comment.