From c8d01197e9d76debf47cfb36e0502311d1c1c1e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Melissa=20Weber=20Mendon=C3=A7a?= Date: Sat, 6 Jul 2024 10:03:06 -0300 Subject: [PATCH 1/5] Enable multiline tags --- docs/conf.py | 2 +- docs/configuration.rst | 12 ---- docs/examples/index.rst | 1 + docs/examples/multiline.rst | 18 ++++++ src/sphinx_tags/__init__.py | 107 +++++++++++++++++++++++++++++------- 5 files changed, 106 insertions(+), 34 deletions(-) create mode 100644 docs/examples/multiline.rst diff --git a/docs/conf.py b/docs/conf.py index 6e5968a..e04d61f 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -35,7 +35,7 @@ extensions = ["sphinx_design", "sphinx_tags", "nbsphinx", "myst_parser"] tags_create_tags = True -tags_create_badges = True +tags_create_badges = False # tags_output_dir = "_tags" # default tags_overview_title = "All tags" # default: "Tags overview" tags_extension = ["rst", "md", "ipynb"] # default: ["rst"] diff --git a/docs/configuration.rst b/docs/configuration.rst index 8ab3649..fcc00de 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -106,18 +106,6 @@ Special characters Tags can contain spaces and special characters such as emoji. In that case, the tag will be normalized when processed. See our :doc:`examples/examples` for more details. -Multiple lines of tags ----------------------- - -Tags can be passed in either as arguments or in the body of the directive: - -.. code-block:: rst - - .. tags:: - - tag1, tag2, tag3, - tag4, tag5, tag6, - Usage with sphinx-autobuild --------------------------- diff --git a/docs/examples/index.rst b/docs/examples/index.rst index 33947cc..4d86616 100644 --- a/docs/examples/index.rst +++ b/docs/examples/index.rst @@ -5,3 +5,4 @@ Usage examples examples raw-cells + multiline diff --git a/docs/examples/multiline.rst b/docs/examples/multiline.rst new file mode 100644 index 0000000..e8d2532 --- /dev/null +++ b/docs/examples/multiline.rst @@ -0,0 +1,18 @@ +Multiline tags +============== + +Since sphinx-tags 0.4.0, we now support multiline tag blocks. This means you can write + +.. code-block:: rst + + .. tags:: + + several, different, tags, can be, added, + as long as, they are, separated + +to obtain + +.. tags:: + + several, different, tags, can be, added, + as long as, they are, separated diff --git a/src/sphinx_tags/__init__.py b/src/sphinx_tags/__init__.py index afb9a91..4993653 100644 --- a/src/sphinx_tags/__init__.py +++ b/src/sphinx_tags/__init__.py @@ -13,8 +13,9 @@ from sphinx.util.docutils import SphinxDirective from sphinx.util.logging import getLogger from sphinx.util.rst import textwidth +from sphinx.util.matching import get_matching_files -__version__ = "0.3.1" +__version__ = "0.4dev" logger = getLogger("sphinx-tags") @@ -26,11 +27,25 @@ class TagLinks(SphinxDirective): See also https://docutils.sourceforge.io/docs/howto/rst-directives.html + This directive can be used with arguments and with content. + + 1. With arguments: + + .. raw:: rst + .. tags:: tag1, tag2, tag3 + + 2. With (multiline) content: + + .. raw:: rst + .. tags:: + + tag1, tag2, + tag3 """ # Sphinx directive class attributes required_arguments = 0 - optional_arguments = 1 # Arbitrary, split on seperator + optional_arguments = 1 # Arbitrary, split on separator final_argument_whitespace = True has_content = True final_argument_whitespace = True @@ -41,14 +56,21 @@ def run(self): if not (self.arguments or self.content): raise ExtensionError("No tags passed to 'tags' directive.") - tagline = [] + page_tags = [] # normalize white space and remove "\n" if self.arguments: - tagline.extend(self.arguments[0].split()) + page_tags.extend( + [_normalize_tag(tag) for tag in self.arguments[0].split(",")] + ) if self.content: - tagline.extend((" ".join(self.content)).strip().split()) - - tags = [tag.strip() for tag in (" ".join(tagline)).split(self.separator)] + # self.content: StringList(['different, tags,', 'separated'], + # items=[(path, lineno), (path, lineno)]) + page_tags.extend( + [_normalize_tag(tag) for tag in ",".join(self.content).split(",")] + ) + # Remove empty elements from page_tags + # (can happen after _normalize_tag()) + page_tags = list(filter(None, page_tags)) tag_dir = Path(self.env.app.srcdir) / self.env.app.config.tags_output_dir result = nodes.paragraph() @@ -59,7 +81,7 @@ def run(self): current_doc_dir = Path(self.env.doc2path(self.env.docname)).parent relative_tag_dir = Path(os.path.relpath(tag_dir, current_doc_dir)) - for tag in tags: + for tag in page_tags: count += 1 # We want the link to be the path to the _tags folder, relative to # this document's path where @@ -70,7 +92,7 @@ def run(self): # | # - current_doc_path - file_basename = _normalize_tag(tag) + file_basename = _normalize_tag(tag, dashes=True) if self.env.app.config.tags_create_badges: result += self._get_badge_node(tag, file_basename, relative_tag_dir) @@ -78,11 +100,11 @@ def run(self): else: result += self._get_plaintext_node(tag, file_basename, relative_tag_dir) tag_separator = f"{self.separator} " - if not count == len(tags): + if not count == len(page_tags): result += nodes.inline(text=tag_separator) # register tags to global metadata for document - self.env.metadata[self.env.docname]["tags"] = tags + self.env.metadata[self.env.docname]["tags"] = page_tags return [result] @@ -132,7 +154,7 @@ class Tag: def __init__(self, name): self.items = [] self.name = name - self.file_basename = _normalize_tag(name) + self.file_basename = _normalize_tag(name, dashes=True) def create_file( self, @@ -214,9 +236,45 @@ def create_file( class Entry: """Tags to pages map""" - def __init__(self, entrypath: Path, tags: list): + def __init__(self, entrypath: Path): self.filepath = entrypath - self.tags = tags + # self.tags = tags + # Read tags (for the first time) to create the tag pages + self.lines = self.filepath.read_text(encoding="utf8").split("\n") + if self.filepath.suffix == ".rst": + tagstart = ".. tags::" + tagend = "" # empty line + elif self.filepath.suffix == ".md": + tagstart = "```{tags}" + tagend = "```" + elif self.filepath.suffix == ".ipynb": + tagstart = '".. tags::' + tagend = "]" + else: + raise ValueError( + "Unknown file extension. Currently, only .rst, .md .ipynb are supported." + ) + + # tagline = [line for line in self.lines if tagstart in line] + # tagblock is all content until the next new empty line + tagblock = [] + reading = False + for line in self.lines: + if tagstart in line: + reading = True + line = line.split(tagstart)[1] + tagblock.extend(line.split(",")) + else: + if reading and line.strip() == tagend: + # tagblock now contains at least one tag + if tagblock != [""]: + break + if reading: + tagblock.extend(line.split(",")) + + self.tags = [] + if tagblock: + self.tags = [tag.strip().rstrip('"') for tag in tagblock if tag != ""] def assign_to_tags(self, tag_dict): """Append ourself to tags""" @@ -230,13 +288,16 @@ def relpath(self, root_dir) -> str: return Path(os.path.relpath(self.filepath, root_dir)).as_posix() -def _normalize_tag(tag: str) -> str: +def _normalize_tag(tag: str, dashes: bool = False) -> str: """Normalize a tag name to use in output filenames and tag URLs. Replace whitespace and other non-alphanumeric characters with dashes. Example: 'Tag:with (special characters) ' -> 'tag-with-special-characters' """ - return re.sub(r"[\s\W]+", "-", tag).lower().strip("-") + char = " " + if dashes: + char = "-" + return re.sub(r"[\s\W]+", char, tag).lower().strip(char) def tagpage(tags, outdir, title, extension, tags_index_head): @@ -295,11 +356,15 @@ def assign_entries(app): pages = [] tags = {} - for docname in app.env.found_docs: - doctags = app.env.metadata[docname].get("tags", None) - if doctags is None: - continue # skip if no tags - entry = Entry(app.env.doc2path(docname), doctags) + # Get document paths in the project that match specified file extensions + doc_paths = get_matching_files( + app.srcdir, + include_patterns=[f"**.{extension}" for extension in app.config.tags_extension], + exclude_patterns=app.config.exclude_patterns, + ) + + for path in doc_paths: + entry = Entry(Path(app.srcdir) / path) entry.assign_to_tags(tags) pages.append(entry) From f616f9b542eaad5222c05658f4965dfaee6fae73 Mon Sep 17 00:00:00 2001 From: Jordan Cook Date: Sun, 7 Jul 2024 13:22:41 -0500 Subject: [PATCH 2/5] Fix tags with extra leading/trailing/consecutive whitespace --- src/sphinx_tags/__init__.py | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/src/sphinx_tags/__init__.py b/src/sphinx_tags/__init__.py index 4993653..1836b3c 100644 --- a/src/sphinx_tags/__init__.py +++ b/src/sphinx_tags/__init__.py @@ -12,8 +12,8 @@ from sphinx.errors import ExtensionError from sphinx.util.docutils import SphinxDirective from sphinx.util.logging import getLogger -from sphinx.util.rst import textwidth from sphinx.util.matching import get_matching_files +from sphinx.util.rst import textwidth __version__ = "0.4dev" @@ -60,13 +60,16 @@ def run(self): # normalize white space and remove "\n" if self.arguments: page_tags.extend( - [_normalize_tag(tag) for tag in self.arguments[0].split(",")] + [_normalize_display_tag(tag) for tag in self.arguments[0].split(",")] ) if self.content: # self.content: StringList(['different, tags,', 'separated'], # items=[(path, lineno), (path, lineno)]) page_tags.extend( - [_normalize_tag(tag) for tag in ",".join(self.content).split(",")] + [ + _normalize_display_tag(tag) + for tag in ",".join(self.content).split(",") + ] ) # Remove empty elements from page_tags # (can happen after _normalize_tag()) @@ -153,7 +156,7 @@ class Tag: def __init__(self, name): self.items = [] - self.name = name + self.name = _normalize_display_tag(name) self.file_basename = _normalize_tag(name, dashes=True) def create_file( @@ -274,7 +277,7 @@ def __init__(self, entrypath: Path): self.tags = [] if tagblock: - self.tags = [tag.strip().rstrip('"') for tag in tagblock if tag != ""] + self.tags = [_normalize_display_tag(tag).rstrip('"') for tag in tagblock if tag != ""] def assign_to_tags(self, tag_dict): """Append ourself to tags""" @@ -300,6 +303,14 @@ def _normalize_tag(tag: str, dashes: bool = False) -> str: return re.sub(r"[\s\W]+", char, tag).lower().strip(char) +def _normalize_display_tag(tag: str) -> str: + """Strip extra whitespace from a tag name for display purposes. + + Example: ' Tag:with (extra whitespace) ' -> 'Tag:with (extra whitespace)' + """ + return re.sub(r"\s+", " ", tag.strip('"').strip()) + + def tagpage(tags, outdir, title, extension, tags_index_head): """Creates Tag overview page. From fe09bceb9cb5cbd3f65fb0a821735cbe36392798 Mon Sep 17 00:00:00 2001 From: Jordan Cook Date: Sun, 7 Jul 2024 13:35:02 -0500 Subject: [PATCH 3/5] Fix issue with leading whitespace in .ipynb files --- src/sphinx_tags/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/sphinx_tags/__init__.py b/src/sphinx_tags/__init__.py index 1836b3c..d955bd8 100644 --- a/src/sphinx_tags/__init__.py +++ b/src/sphinx_tags/__init__.py @@ -263,12 +263,13 @@ def __init__(self, entrypath: Path): tagblock = [] reading = False for line in self.lines: + line = line.strip() if tagstart in line: reading = True line = line.split(tagstart)[1] tagblock.extend(line.split(",")) else: - if reading and line.strip() == tagend: + if reading and line == tagend: # tagblock now contains at least one tag if tagblock != [""]: break @@ -277,7 +278,7 @@ def __init__(self, entrypath: Path): self.tags = [] if tagblock: - self.tags = [_normalize_display_tag(tag).rstrip('"') for tag in tagblock if tag != ""] + self.tags = [_normalize_display_tag(tag) for tag in tagblock if tag != ""] def assign_to_tags(self, tag_dict): """Append ourself to tags""" From 85f00d7b812931ec057b171b168675a19b487593 Mon Sep 17 00:00:00 2001 From: Jordan Cook Date: Sun, 7 Jul 2024 13:52:41 -0500 Subject: [PATCH 4/5] Fix issue with newlines being created as tags in .ipynb files --- src/sphinx_tags/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/sphinx_tags/__init__.py b/src/sphinx_tags/__init__.py index d955bd8..7bdff08 100644 --- a/src/sphinx_tags/__init__.py +++ b/src/sphinx_tags/__init__.py @@ -278,7 +278,7 @@ def __init__(self, entrypath: Path): self.tags = [] if tagblock: - self.tags = [_normalize_display_tag(tag) for tag in tagblock if tag != ""] + self.tags = [_normalize_display_tag(tag) for tag in tagblock if tag] def assign_to_tags(self, tag_dict): """Append ourself to tags""" @@ -309,7 +309,8 @@ def _normalize_display_tag(tag: str) -> str: Example: ' Tag:with (extra whitespace) ' -> 'Tag:with (extra whitespace)' """ - return re.sub(r"\s+", " ", tag.strip('"').strip()) + tag = tag.replace("\\n", "\n").strip('"').strip() + return re.sub(r"\s+", " ", tag) def tagpage(tags, outdir, title, extension, tags_index_head): From 7f826711d9b9210750b63baa434862896e6768fb Mon Sep 17 00:00:00 2001 From: Jordan Cook Date: Sun, 7 Jul 2024 13:54:24 -0500 Subject: [PATCH 5/5] Re-enable tag badges in our own docs --- docs/conf.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index e04d61f..79a769c 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -12,6 +12,7 @@ # import os import sys + from sphinx_tags import __version__ sys.path.insert(0, os.path.abspath("../src")) @@ -35,7 +36,7 @@ extensions = ["sphinx_design", "sphinx_tags", "nbsphinx", "myst_parser"] tags_create_tags = True -tags_create_badges = False +tags_create_badges = True # tags_output_dir = "_tags" # default tags_overview_title = "All tags" # default: "Tags overview" tags_extension = ["rst", "md", "ipynb"] # default: ["rst"]