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..7013c02 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -132,3 +132,5 @@ files so it doesn't get stuck in a loop. Example: sphinx-autobuild docs docs/_build/html --ignore '**/_tags/*' If you have set ``tags_output_dir`` to a different path, use that instead of ``_tags``. + +.. tags:: tag documentation \ No newline at end of file diff --git a/docs/index.rst b/docs/index.rst index e07ede1..c21792c 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -26,4 +26,4 @@ Check out the `list of projects that use this extension str: return "primary" -class Tag: - """A tag contains entries""" - - def __init__(self, name): - self.items = [] - self.name = name - self.file_basename = _normalize_tag(name) - - def __repr__(self): - return f"Tag({self.name}), {len(self.items)} items: {self.items}" - - def create_file( - self, - items, - extension, - tags_output_dir, - srcdir, - tags_page_title, - tags_page_header, - ): - """Create file with list of documents associated with a given tag in - toctree format. - - This file is reached as a link from the tag name in each documentation - file, or from the tag overview page. - - If we are using md files, generate and md file; otherwise, go with rst. - - Parameters - ---------- - - tags_output_dir : Path - path where the file for this tag will be created - items : list - list of files associated with this tag (instance of Entry) - extension : {["rst"], ["md"], ["rst", "md"]} - list of file extensions used. - srcdir : str - root folder for the documentation (usually, project/docs) - tags_page_title: str - the title of the tag page, after which the tag is listed (e.g. "Tag: programming") - tags_page_header: str - the words after which the pages with the tag are listed (e.g. "With this tag: Hello World") - tag_intro_text: str - the words after which the tags of a given page are listed (e.g. "Tags: programming, python") +class TagsDomain(Domain): + name = "tags" + label = "Tags" - """ - # Get sorted file paths for tag pages, relative to /docs/_tags - tag_page_paths = sorted([i.relpath(srcdir) for i in items]) - ref_label = f"sphx_tag_{self.file_basename}" + roles = {} - content = [] - if "md" in extension: - filename = f"{self.file_basename}.md" - content.append(f"({ref_label})=") - content.append(f"# {tags_page_title}: {self.name}") - content.append("") - content.append("```{toctree}") - content.append("---") - content.append("maxdepth: 1") - content.append(f"caption: {tags_page_header}") - content.append("---") - for path in tag_page_paths: - content.append(f"../{path}") - content.append("```") - else: - filename = f"{self.file_basename}.rst" - header = f"{tags_page_title}: {self.name}" - content.append(f".. _{ref_label}:") - content.append("") - content.append(header) - content.append("#" * textwidth(header)) - content.append("") - content.append(".. toctree::") - content.append(" :maxdepth: 1") - content.append(f" :caption: {tags_page_header}") - content.append("") - for path in tag_page_paths: - content.append(f" ../{path}") + directives = { + "tags": TagLinks, + } - content.append("") - with open( - os.path.join(srcdir, tags_output_dir, filename), "w", encoding="utf8" - ) as f: - f.write("\n".join(content)) - - -class Entry: - """Tags to pages map""" - - # def __init__(self, entrypath: Path, tags: list): - def __init__(self, entrypath: Path): - self.filepath = entrypath - self.lines = self.filepath.read_text(encoding="utf8").split("\n") - if self.filepath.suffix == ".rst": - tagstart = ".. tags::" - tagend = "" - 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 and .ipynb are supported." - ) + # The values defined in initial_data will be copied to + # env.domaindata[domain_name] as the initial data of the domain, and domain + # instances can access it via self.data. + initial_data = { + "tags": [], + "entries": {}, + } - self.tags = read_tags(self.lines, tagstart, tagend) + def get_full_qualified_name(self, node): + print(f"Node: {node}") + return f"tags.{node.arguments[0]}" + def get_objects(self): + yield from self.data["tags"] - def __repr__(self): - return f"Entry({self.filepath}), {self.tags}" + def add_tag(self, tagname, page): + """Add a new tag to the domain.""" + anchor = f"{tagname}" + # Add this page to the list of pages with this tag + if self.data["entries"].get(tagname) is None: + self.data["entries"][tagname] = [page] + else: + self.data["entries"][tagname].append(page) + + # Add this tag to the global list of tags + # name, dispname, type, docname, anchor, priority + self.data["tags"].append((tagname, tagname, "Tag", page, anchor, 0)) + + +def create_file( + app, + tag: tuple, + extension: List[str], + tags_output_dir: Path, + srcdir: str, + tags_page_title: str, + tags_page_header: str, + tag_intro_text: str, +): + """Create file with list of documents associated with a given tag in + toctree format. + + This file is reached as a link from the tag name in each documentation + file, or from the tag overview page. + + If we are using md files, generate and md file; otherwise, go with rst. + + Parameters + ---------- + + tag : tuple + tag name and list of pages associated with this tag + extension : {["rst"], ["md"], ["rst", "md"]} + list of file extensions used. + tags_output_dir : Path + path where the file for this tag will be created + srcdir : str + root folder for the documentation (usually, project/docs) + tags_page_title: str + the title of the tag page, after which the tag is listed (e.g. "Tag: programming") + tags_page_header: str + the words after which the pages with the tag are listed (e.g. "With this tag: Hello World") + tag_intro_text: str + the words after which the tags of a given page are listed (e.g. "Tags: programming, python") - def assign_to_tags(self, tag_dict): - """Append ourself to tags""" - for tag in self.tags: - if tag: - if tag not in tag_dict: - tag_dict[tag] = Tag(tag) - tag_dict[tag].items.append(self) - def relpath(self, root_dir) -> str: - """Get this entry's path relative to the given root directory""" - return Path(os.path.relpath(self.filepath, root_dir)).as_posix() + """ -def read_tags(lines, tagstart, tagend): - """Read tags from a list of lines in a file. + name = tag[0] + file_basename = _normalize_tag(tag[0], dashes=True) - """ - tagline = [line for line in lines if tagstart in line] - # Custom attributes - separator = "," + # Get sorted file paths for tag pages, relative to /docs/_tags + tag_page_paths = sorted([os.path.relpath(i, srcdir) for i in tag[1]]) + ref_label = f"sphx_tag_{file_basename}" - tags = [] - if tagline: - # TODO: This is matching the .. tags:: example inside a code-block - # in configuration.rst - tagline = tagline[0].replace(tagstart, "").rstrip(tagend) - tags = [" ".join(tag.strip().split()) for tag in tagline.split(separator)] - tags = [tag for tag in tags if tag != ""] - return tags + content = [] + if "md" in extension: + filename = f"{file_basename}.md" + content.append(f"({ref_label})=") + content.append(f"# {tags_page_title}: {name}") + content.append("") + content.append("```{toctree}") + content.append("---") + content.append("maxdepth: 1") + content.append(f"caption: {tags_page_header}") + content.append("---") + for path in tag_page_paths: + content.append(f"../{path}") + content.append("```") + else: + filename = f"{file_basename}.rst" + header = f"{tags_page_title}: {name}" + content.append(f".. _{ref_label}:") + content.append("") + content.append(header) + content.append("#" * textwidth(header)) + content.append("") + content.append(".. toctree::") + content.append(" :maxdepth: 1") + content.append(f" :caption: {tags_page_header}") + content.append("") + for path in tag_page_paths: + content.append(f" ../{path}") + + content.append("") + with open( + os.path.join(srcdir, tags_output_dir, filename), "w", encoding="utf8" + ) as f: + f.write("\n".join(content)) -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,7 +288,8 @@ def tagpage(tags, outdir, title, extension, tags_index_head): """ - tags = list(tags.values()) + print(f"Tags: {tags=}") + print(f"outdir: {outdir=}") if "md" in extension: content = [] @@ -309,8 +303,9 @@ def tagpage(tags, outdir, title, extension, tags_index_head): content.append(f"caption: {tags_index_head}") content.append("maxdepth: 1") content.append("---") - for tag in sorted(tags, key=lambda t: t.name): - content.append(f"{tag.name} ({len(tag.items)}) <{tag.file_basename}>") + for name, pages in tags.items(): + file_basename = _normalize_tag(name, dashes=True) + content.append(f"{name} ({len(pages)}) <{file_basename}>") content.append("```") content.append("") filename = os.path.join(outdir, "tagsindex.md") @@ -328,10 +323,9 @@ def tagpage(tags, outdir, title, extension, tags_index_head): content.append(f" :caption: {tags_index_head}") content.append(" :maxdepth: 1") content.append("") - for tag in sorted(tags, key=lambda t: t.name): - content.append( - f" {tag.name} ({len(tag.items)}) <{tag.file_basename}.rst>" - ) + for name, pages in tags.items(): + file_basename = _normalize_tag(name, dashes=True) + content.append(f" {name} ({len(pages)}) <{file_basename}.rst>") content.append("") filename = os.path.join(outdir, "tagsindex.rst") @@ -339,37 +333,6 @@ def tagpage(tags, outdir, title, extension, tags_index_head): f.write("\n".join(content)) -def assign_entries(app): - """Assign all found entries to their tag. - - Returns - ------- - tags : dict - Dictionary of tags, keyed by tag name - pages : list - List of Entry objects - """ - pages = [] - tags = {} - - 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) - # docname is path without the file extension - # docname = path.split(".", 1)[0] - # register tags to global metadata for document - # app.env.metadata[docname]["tags"] = tags - - return tags, pages - - def update_tags(app): """Update tags according to pages found""" if app.config.tags_create_tags: @@ -383,22 +346,24 @@ def update_tags(app): os.remove(os.path.join(app.srcdir, tags_output_dir, file)) # Create pages for each tag - tags, pages = assign_entries(app) - - for tag in tags.values(): - if tag: - tag.create_file( - [item for item in pages if tag.name in item.tags], - app.config.tags_extension, - tags_output_dir, - app.srcdir, - app.config.tags_page_title, - app.config.tags_page_header, - ) + global_tags = env.get_domain("tags").data["entries"] + logger.info(f"Global tags: {global_tags=}", color="green") + + for tag in global_tags.items(): + create_file( + app, + tag, + app.config.tags_extension, + tags_output_dir, + app.srcdir, + app.config.tags_page_title, + app.config.tags_page_header, + app.config.tags_intro_text, + ) # Create tags overview page tagpage( - tags, + global_tags, os.path.join(app.srcdir, tags_output_dir), app.config.tags_overview_title, app.config.tags_extension, @@ -410,6 +375,9 @@ def update_tags(app): "Tags were not created (tags_create_tags=False in conf.py)", color="white" ) + # Return iterable of docnames to re-read + return os.listdir(os.path.join(app.srcdir, tags_output_dir)) + def setup(app): """Setup for Sphinx.""" @@ -438,11 +406,11 @@ def setup(app): ) # Update tags - # TODO: tags should be updated after sphinx-gallery is generated, and the - # gallery is also connected to builder-inited. Are there situations when - # this will not work? - app.connect("builder-inited", update_tags) + # Tags should be updated after sphinx-gallery is generated, on + # builder-inited + app.connect("source-read", update_tags) app.add_directive("tags", TagLinks) + app.add_domain(TagsDomain) return { "version": __version__,