mirror of
https://github.com/beetbox/beets.git
synced 2025-12-07 17:16:07 +01:00
168 lines
6 KiB
Python
168 lines
6 KiB
Python
import os
|
|
from pathlib import Path
|
|
|
|
from docutils import nodes
|
|
from docutils.parsers.rst import Directive, directives
|
|
|
|
|
|
class PluginListDirective(Directive):
|
|
"""Directive to list all .rst files in a given folder.
|
|
|
|
Along with their top-level header, with optional extra user-defined entries.
|
|
|
|
Usage:
|
|
.. filelist::
|
|
:path: path/to/folder
|
|
:exclude: file1.rst, file2.rst
|
|
:extra:
|
|
overview.rst # file link with implicit title from filename
|
|
Overview: overview.rst # file link with custom title
|
|
:ref:`user-guide` # arbitrary reST reference
|
|
"""
|
|
|
|
has_content = False
|
|
required_arguments = 0
|
|
optional_arguments = 0
|
|
|
|
option_spec = {
|
|
"path": directives.unchanged_required,
|
|
"exclude": directives.unchanged,
|
|
"extra": directives.unchanged,
|
|
}
|
|
|
|
def _extract_title(self, path: Path):
|
|
"""Extract the first section title from an rst file."""
|
|
with open(path, encoding="utf-8") as f:
|
|
lines = [ln.rstrip() for ln in f]
|
|
for idx, line in enumerate(lines):
|
|
if not line:
|
|
continue
|
|
# Check for underline-style title
|
|
if idx + 1 < len(lines) and set(lines[idx + 1]) <= set(
|
|
"= - `:'~^_*+#<>"
|
|
):
|
|
underline = lines[idx + 1]
|
|
if len(underline) >= len(line):
|
|
return line
|
|
# Or overline/underline style
|
|
if idx >= 1 and set(lines[idx - 1]) <= set("= - `:'~^_*+#<>"):
|
|
overline = lines[idx - 1]
|
|
if len(overline) >= len(line):
|
|
return line
|
|
# Fallback: filename without extension
|
|
return os.path.splitext(os.path.basename(path))[0]
|
|
|
|
def _get_current_src(self) -> Path:
|
|
"""Get the current source file path."""
|
|
current_doc = self.state.document.current_source
|
|
if not current_doc:
|
|
raise ValueError("Current document source could not be determined.")
|
|
return Path(current_doc).resolve()
|
|
|
|
def run(self):
|
|
folder_option = self.options.get("path")
|
|
if not folder_option:
|
|
error = self.state_machine.reporter.error(
|
|
'The "path" option is required for the pluginlist directive.',
|
|
nodes.literal_block(self.block_text, self.block_text),
|
|
line=self.lineno,
|
|
)
|
|
return [error]
|
|
|
|
# Resolve folder path relative to current doc file
|
|
cur_path = self._get_current_src()
|
|
target_folder = cur_path.joinpath(folder_option).resolve().parent
|
|
|
|
if not os.path.isdir(target_folder):
|
|
error = self.state_machine.reporter.error(
|
|
f'Path "{folder_option}" resolved to "{target_folder}". '
|
|
"Could not found or is not a directory.",
|
|
nodes.literal_block(self.block_text, self.block_text),
|
|
line=self.lineno,
|
|
)
|
|
return [error]
|
|
|
|
excludes_raw = self.options.get("exclude", "")
|
|
excludes = [x.strip() for x in excludes_raw.split(",") if x.strip()]
|
|
|
|
# Find .rst files, excluding specified
|
|
files = [
|
|
f
|
|
for f in os.listdir(target_folder)
|
|
if f.endswith(".rst") and f not in excludes
|
|
]
|
|
|
|
refs = []
|
|
for filename in files:
|
|
# Reference to the rst file
|
|
refuri = (
|
|
os.path.splitext(os.path.join(folder_option, filename))[
|
|
0
|
|
].replace(os.sep, "/")
|
|
+ ".html"
|
|
)
|
|
# Title for the link
|
|
title = self._extract_title(target_folder.joinpath(filename))
|
|
|
|
ref = nodes.reference("", title, internal=True, refuri=refuri)
|
|
refs.append(ref)
|
|
|
|
# Extra entries into refs
|
|
extra_option = self.options.get("extra", "")
|
|
if extra_option:
|
|
from docutils.statemachine import ViewList
|
|
|
|
for line in extra_option.splitlines():
|
|
entry = line.strip()
|
|
if not entry:
|
|
continue
|
|
|
|
para = nodes.paragraph()
|
|
|
|
# If entry is pure reST (contains role/backticks and no file .rst)
|
|
if (
|
|
"`" in entry or entry.strip().startswith(":ref")
|
|
) and ".rst" not in entry:
|
|
vl = ViewList()
|
|
vl.append(entry, self.block_text)
|
|
self.state.nested_parse(vl, self.content_offset, para)
|
|
else:
|
|
# file link: either 'file.rst' or 'Title: file.rst'
|
|
if ":" in entry:
|
|
title, target = [p.strip() for p in entry.split(":", 1)]
|
|
else:
|
|
target = entry
|
|
title = Path(entry).stem
|
|
if target.endswith(".rst"):
|
|
title = self._extract_title(
|
|
target_folder.joinpath(target)
|
|
)
|
|
rel = Path(self.options["path"]) / target
|
|
refuri = str(rel.with_suffix(".html")).replace(
|
|
os.sep, "/"
|
|
)
|
|
ref = nodes.reference(
|
|
"", title, internal=True, refuri=refuri
|
|
)
|
|
para += ref
|
|
else:
|
|
# fallback parse
|
|
vl = ViewList()
|
|
vl.append(entry, self.block_text)
|
|
self.state.nested_parse(vl, self.content_offset, para)
|
|
|
|
refs.append(para)
|
|
|
|
# Sort refs
|
|
refs.sort(key=lambda x: x.astext().lower())
|
|
|
|
# Build bullet list of links
|
|
bullet_list = nodes.bullet_list()
|
|
for ref in refs:
|
|
item = nodes.list_item()
|
|
para = nodes.paragraph()
|
|
para += ref
|
|
item += para
|
|
bullet_list += item
|
|
|
|
return [bullet_list]
|