diff --git a/example/pages/subdir/subsubpage.md b/example/pages/subdir/subsubpage.md index 4eb92d54..b4431ce1 100644 --- a/example/pages/subdir/subsubpage.md +++ b/example/pages/subdir/subsubpage.md @@ -4,3 +4,6 @@ title: Subpage in subdirectory This page is deeper in the hierarchy. You can use `|page|` in URLs to link to other places in the page hierarchy, [such as this other subpage](|page|/subpage1.html) + +You can link to the API documentation with the Ford `[[link]]` style: +[[ford_test_program]] diff --git a/ford/__init__.py b/ford/__init__.py index 51ba1fe8..86775bff 100755 --- a/ford/__init__.py +++ b/ford/__init__.py @@ -415,8 +415,6 @@ def main(proj_data: ProjectSettings, proj_docs: str): ) sys.exit(1) - base_url = ".." if proj_data.relative else proj_data.project_url - print(" Correlating information from different parts of your project...") correlate_time_start = time.time() project.correlate() @@ -437,20 +435,17 @@ def main(proj_data: ProjectSettings, proj_docs: str): md = MetaMarkdown( proj_data.md_base_dir, + base_url=proj_data.project_url, extensions=proj_data.md_extensions, aliases=aliases, project=project, - relative=proj_data.relative, - base_url=proj_data.project_url, ) # Convert the documentation from Markdown to HTML - proj_docs = md.reset().convert(proj_docs) - project.markdown(md, base_url) + proj_docs = md.reset().convert(proj_docs, path=proj_data.project_url) + project.markdown(md) # Convert summaries and descriptions to HTML - if proj_data.relative: - ford.sourceform.set_base_url(".") if proj_data.summary is not None: proj_data.summary = md.convert(proj_data.summary) if proj_data.author_description is not None: diff --git a/ford/_markdown.py b/ford/_markdown.py index acc25345..acf7ce70 100644 --- a/ford/_markdown.py +++ b/ford/_markdown.py @@ -36,8 +36,6 @@ class MetaMarkdown(Markdown): Dictionary of text aliases project : Ford project instance - relative : - Should internal URLs be relative base_url : Base/project URL for relative links (required if ``relative`` is True) @@ -47,12 +45,11 @@ class MetaMarkdown(Markdown): def __init__( self, md_base: PathLike = ".", + base_url: PathLike = ".", extensions: Optional[List[Union[str, Extension]]] = None, extension_configs: Optional[Dict[str, Dict]] = None, aliases: Optional[Dict[str, str]] = None, project: Optional[Project] = None, - relative: bool = False, - base_url: Optional[PathLike] = None, ): """make thing""" @@ -71,10 +68,9 @@ def __init__( if project is not None: default_extensions.append(FordLinkExtension(project=project)) - if relative: - if base_url is None: - raise ValueError("Expected path for base_url, got None") - default_extensions.append(RelativeLinksExtension(base_url=base_url)) + self.base_url = Path(base_url) + if base_url != ".": + default_extensions.append(RelativeLinksExtension()) if extensions is None: extensions = [] @@ -116,7 +112,14 @@ def convert( """ self.current_context = context - self.current_path = path + if ( + path is None + and context is not None + and (url := context.get_url()) is not None + ): + self.current_path = self.base_url / Path(url).parent + else: + self.current_path = path return super().convert(source) @@ -235,7 +238,14 @@ def find_child(context): link.text = name return link - link.attrib["href"] = item.get_url() + if (item_url := item.get_url()) is None: + # This is really to keep mypy happy + raise RuntimeError(f"Found item {name} but no url") + + # Make sure links are relative to base url + full_url = self.md.base_url / item_url + rel_url = relpath(full_url, self.md.current_path) + link.attrib["href"] = str(rel_url) link.text = item.name return link @@ -262,8 +272,8 @@ class RelativeLinksTreeProcessor(Treeprocessor): md: MetaMarkdown - def __init__(self, md: MetaMarkdown, base_url: Path): - self.base_url = base_url.resolve() + def __init__(self, md: MetaMarkdown): + self.base_url = md.base_url.resolve() super().__init__(md) def _fix_attrib(self, tag: Element, attrib: str): @@ -290,11 +300,7 @@ class RelativeLinksExtension(Extension): """Markdown extension to register `RelativeLinksTreeProcessor`""" def __init__(self, **kwargs): - self.config = {"base_url": [kwargs["base_url"], "Base URL of project"]} super().__init__(**kwargs) def extendMarkdown(self, md: MetaMarkdown): # type: ignore[override] - base_url: Path = self.getConfig("base_url") - md.treeprocessors.register( - RelativeLinksTreeProcessor(md, base_url=base_url), "relative_links", 5 - ) + md.treeprocessors.register(RelativeLinksTreeProcessor(md), "relative_links", 5) diff --git a/ford/external_project.py b/ford/external_project.py index c50bd59f..136ef8d0 100644 --- a/ford/external_project.py +++ b/ford/external_project.py @@ -61,7 +61,7 @@ def obj2dict(intObj): return None extDict = { "name": intObj.name, - "external_url": intObj.get_url(), + "external_url": f"./{intObj.get_url()}", "obj": intObj.obj, } if hasattr(intObj, "proctype"): diff --git a/ford/fortran_project.py b/ford/fortran_project.py index 85153e56..4adc6fbc 100755 --- a/ford/fortran_project.py +++ b/ford/fortran_project.py @@ -32,7 +32,6 @@ from ford.console import warn from ford.external_project import load_external_modules from ford.utils import ProgressBar -import ford.sourceform from ford.sourceform import ( _find_in_list, FortranBase, @@ -340,11 +339,6 @@ def filter_modules(entity) -> List[FortranModule]: if not isinstance(container, str): container.prune() - if self.settings.project_url == ".": - url = ".." - else: - url = self.settings.project_url - # Mapping of various entity containers in code units to the # corresponding container in the project CONTAINERS = { @@ -386,11 +380,10 @@ def sum_lines(*argv, **kwargs): self.prog_lines = sum_lines(self.programs) self.block_lines = sum_lines(self.blockdata) - def markdown(self, md, base_url=".."): + def markdown(self, md): """ Process the documentation with Markdown to produce HTML. """ - ford.sourceform.set_base_url(base_url) if self.settings.warn: print() diff --git a/ford/output.py b/ford/output.py index baf09f21..3f097a43 100644 --- a/ford/output.py +++ b/ford/output.py @@ -31,11 +31,13 @@ import pathlib import time from typing import List, Union, Callable, Type, Tuple +from warnings import simplefilter +from bs4 import BeautifulSoup, MarkupResemblesLocatorWarning import jinja2 from ford.console import warn -import ford.sourceform +from ford.sourceform import FortranBase import ford.tipue_search from ford.utils import ProgressBar from ford.graphs import graphviz_installed, GraphManager @@ -49,6 +51,9 @@ ) env.globals["path"] = os.path # this lets us call path.* in templates +# Ignore bs4 warning about parsing strings that look like filenames +simplefilter("ignore", MarkupResemblesLocatorWarning) + def is_more_than_one(collection): return collection > 1 @@ -71,8 +76,30 @@ def meta(entity, item): return getattr(entity.meta, item, None) +def relative_url(entity: Union[FortranBase, str], page_url: pathlib.Path) -> str: + """Convert any links with absolute paths to the output directory + to relative paths to ``page_url`` + """ + if isinstance(entity, str) and "/" not in entity or entity is None: + return entity + + # Find first link in `entity` and get the path. If `entity` + # doesn't have any links, it might be a URL itself that needs + # fixing + link_str = str(entity) + link = BeautifulSoup(link_str, features="html.parser").a + if link is not None: + link_path = str(pathlib.Path(str(link["href"])).resolve()) + else: + link_path = link_str + + new_path = os.path.relpath(link_path, page_url.parent) + return link_str.replace(link_path, new_path) + + env.tests["more_than_one"] = is_more_than_one env.filters["meta"] = meta +env.filters["relurl"] = relative_url USER_WRITABLE_ONLY = 0o755 @@ -95,6 +122,9 @@ def __init__(self, settings: ProjectSettings, proj_docs: str, project, pagetree) # rid of None values self.data = {k: v for k, v in asdict(settings).items() if v is not None} self.data["pages"] = pagetree + # Remove "project_url" so we can set it as a relative path for + # each page individually and not have it clobbered by this dict + del self.data["project_url"] self.lists: List[ListPage] = [] self.docs = [] self.njobs = settings.parallel @@ -307,9 +337,15 @@ def __init__(self, data, proj, obj=None): self.meta = getattr(obj, "meta", EntitySettings()) self.out_dir = self.data["output_dir"] self.page_dir = self.out_dir / "page" + self.relative = data["relative"] + self.project_url = ( + os.path.relpath(proj.settings.project_url, self.outfile.parent) + if self.relative + else proj.settings.project_url + ) @property - def html(self): + def html(self) -> str: """Wrapper for only doing the rendering on request (drastically reduces memory)""" try: return self.render(self.data, self.proj, self.obj) @@ -327,6 +363,19 @@ def outfile(self) -> pathlib.Path: def writeout(self) -> None: self.outfile.write_bytes(self.html.encode("utf8")) + @property + def template_path(self) -> str: + """Path to page template file (relative to 'templates/')""" + raise NotImplementedError() + + @property + def template(self): + """Jinja template loaded from `template_path` with globals set""" + return env.get_template( + self.template_path, + globals=dict(page_url=self.outfile, project_url=self.project_url), + ) + def render(self, data, proj, obj): """ Get the HTML for the page. This method must be overridden. Arguments @@ -337,29 +386,20 @@ def render(self, data, proj, obj): class ListTopPage(BasePage): - @property - def list_page(self): - raise NotImplementedError("ListTopPage subclass missing 'list_page' property") - @property def outfile(self): - return self.out_dir / self.list_page + return self.out_dir / self.template_path def render(self, data, proj, obj): - if data["relative"]: - data["project_url"] = "." - ford.sourceform.set_base_url(".") - ford.pagetree.set_base_url(".") - template = env.get_template(self.list_page) - return template.render(data, project=proj, proj_docs=obj) + return self.template.render(data, project=proj, proj_docs=obj) class IndexPage(ListTopPage): - list_page = "index.html" + template_path = "index.html" class SearchPage(ListTopPage): - list_page = "search.html" + template_path = "search.html" class ListPage(BasePage): @@ -367,61 +407,52 @@ class ListPage(BasePage): def out_page(self): raise NotImplementedError("ListPage subclass missing 'out_page' property") - @property - def list_page(self): - raise NotImplementedError("ListPage subclass missing 'list_page' property") - @property def outfile(self): return self.out_dir / "lists" / self.out_page def render(self, data, proj, obj): - if data["relative"]: - data["project_url"] = ".." - ford.sourceform.set_base_url("..") - ford.pagetree.set_base_url("..") - template = env.get_template(self.list_page) - return template.render(data, project=proj) + return self.template.render(data, project=proj) class ProcList(ListPage): out_page = "procedures.html" - list_page = "proc_list.html" + template_path = "proc_list.html" class FileList(ListPage): out_page = "files.html" - list_page = "file_list.html" + template_path = "file_list.html" class ModList(ListPage): out_page = "modules.html" - list_page = "mod_list.html" + template_path = "mod_list.html" class ProgList(ListPage): out_page = "programs.html" - list_page = "prog_list.html" + template_path = "prog_list.html" class TypeList(ListPage): out_page = "types.html" - list_page = "types_list.html" + template_path = "types_list.html" class AbsIntList(ListPage): out_page = "absint.html" - list_page = "absint_list.html" + template_path = "absint_list.html" class BlockList(ListPage): out_page = "blockdata.html" - list_page = "block_list.html" + template_path = "block_list.html" class NamelistList(ListPage): out_page = "namelists.html" - list_page = "namelist_list.html" + template_path = "namelist_list.html" class DocPage(BasePage): @@ -429,10 +460,6 @@ class DocPage(BasePage): Abstract class to be inherited by all pages for items in the code. """ - @property - def page_path(self): - raise NotImplementedError("DocPage subclass missing 'page_path'") - @property def payload_key(self): raise NotImplementedError("DocPage subclass missing 'payload_key'") @@ -450,65 +477,62 @@ def outfile(self): return self.out_dir / self.obj.get_dir() / self.object_page def render(self, data, project, object): - if data["relative"]: - data["project_url"] = ".." - ford.sourceform.set_base_url("..") - ford.pagetree.set_base_url("..") - template = env.get_template(self.page_path) try: - return template.render(data, project=project, **{self.payload_key: object}) + return self.template.render( + data, project=project, **{self.payload_key: object} + ) except jinja2.exceptions.TemplateError: print(f"Error rendering page '{self.outfile}'") raise class FilePage(DocPage): - page_path = "file_page.html" + template_path = "file_page.html" payload_key = "src" class TypePage(DocPage): - page_path = "type_page.html" + template_path = "type_page.html" payload_key = "dtype" class AbsIntPage(DocPage): - page_path = "nongenint_page.html" + template_path = "nongenint_page.html" payload_key = "interface" class ModulePage(DocPage): - page_path = "mod_page.html" + template_path = "mod_page.html" payload_key = "module" class ProgPage(DocPage): - page_path = "prog_page.html" + template_path = "prog_page.html" payload_key = "program" class BlockPage(DocPage): - page_path = "block_page.html" + template_path = "block_page.html" payload_key = "blockdat" class ProcedurePage(DocPage): - page_path = "proc_page.html" + template_path = "proc_page.html" payload_key = "procedure" class GenericInterfacePage(DocPage): - page_path = "genint_page.html" + template_path = "genint_page.html" payload_key = "interface" class InterfacePage(DocPage): - page_path = "nongenint_page.html" + template_path = "nongenint_page.html" payload_key = "interface" class NamelistPage(DocPage): - page_path = "namelist_page.html" + template_path = "namelist_page.html" payload_key = "namelist" @@ -522,6 +546,8 @@ def ProcPage(data, proj, obj): class PagetreePage(BasePage): + template_path = "info_page.html" + @property def loc(self): return pathlib.Path("page") / self.obj.path @@ -531,18 +557,7 @@ def outfile(self): return self.page_dir / self.obj.path def render(self, data, proj, obj): - if data["relative"]: - base_url = ("../" * len(obj.hierarchy))[:-1] - if obj.filename.stem == "index": - if len(obj.hierarchy) > 0: - base_url = base_url + "/.." - else: - base_url = ".." - ford.sourceform.set_base_url(base_url) - ford.pagetree.set_base_url(base_url) - data["project_url"] = base_url - template = env.get_template("info_page.html") - return template.render(data, page=obj, project=proj, topnode=obj.topnode) + return self.template.render(data, page=obj, project=proj, topnode=obj.topnode) def writeout(self): if self.obj.filename.stem == "index": diff --git a/ford/pagetree.py b/ford/pagetree.py index 4bfcb360..be0d5a28 100644 --- a/ford/pagetree.py +++ b/ford/pagetree.py @@ -39,8 +39,6 @@ class PageNode: Object representing a page in a tree of pages and subpages. """ - base_url = Path("..") - def __init__( self, md: MetaMarkdown, @@ -52,6 +50,7 @@ def __init__( ): meta, text = meta_preprocessor(dedent(Path(path).read_text(encoding))) self.meta = EntitySettings.from_markdown_metadata(meta, path.stem) + self.base_url = Path(md.base_url) if self.meta.title is None: raise ValueError(f"Page '{path}' has no title metadata") @@ -183,7 +182,3 @@ def get_page_tree( node.files.append(name) return node - - -def set_base_url(url): - PageNode.base_url = Path(url) diff --git a/ford/sourceform.py b/ford/sourceform.py index f677a163..7719f163 100644 --- a/ford/sourceform.py +++ b/ford/sourceform.py @@ -186,9 +186,6 @@ class FortranBase: SPLIT_RE = re.compile(r"\s*,\s*", re.IGNORECASE) SRC_CAPTURE_STR = r"^[ \t]*([\w(),*: \t]+?[ \t]+)?{0}([\w(),*: \t]+?)?[ \t]+{1}[ \t\n,(].*?end[ \t]*{0}[ \t]+{1}[ \t]*?(!.*?)?$" - # ~ this regex is not working for the LINK and DOUBLE_LINK types - - base_url = "" pretty_obj = { "proc": "procedures", "type": "derived types", @@ -227,6 +224,7 @@ def __init__( self.display = [] self.settings = ProjectSettings() + self.base_url = pathlib.Path(self.settings.project_url) self.doc_list = read_docstring(source, self.settings.docmark) self.hierarchy = self._make_hierarchy() self.read_metadata() @@ -299,21 +297,28 @@ def get_dir(self) -> Optional[str]: return None - def get_url(self): + def get_url(self) -> Optional[str]: + """URL of this entity, relative to base URL""" + if hasattr(self, "external_url"): return self.external_url + if loc := self.get_dir(): - return f"{self.base_url}/{loc}/{self.ident}.html" - if isinstance( - self, - ( - FortranBoundProcedure, - FortranCommon, - FortranVariable, - FortranEnum, - FortranFinalProc, - FortranProcedure, - ), + return f"{loc}/{self.ident}.html" + + if ( + isinstance( + self, + ( + FortranBoundProcedure, + FortranCommon, + FortranVariable, + FortranEnum, + FortranFinalProc, + FortranProcedure, + ), + ) + and self.parent is not None ): if parent_url := self.parent.get_url(): if "#" in parent_url: @@ -322,6 +327,17 @@ def get_url(self): return None return None + @property + def full_url(self) -> Optional[str]: + """URL of this entity, including the base URL""" + if hasattr(self, "external_url"): + return self.external_url + + if (url := self.get_url()) is not None: + return str(self.base_url / url) + + return None + def lines_description(self, total, total_all=0, obj=None): if not obj: obj = self.obj @@ -344,8 +360,7 @@ def anchor(self) -> str: return f"{self.obj}-{quote(self.ident)}" def __str__(self): - url = self.get_url() - if url and getattr(self, "visible", True): + if (url := self.full_url) and getattr(self, "visible", True): name = self.name or "unnamed" return f"{name}" return self.name or "" @@ -411,7 +426,7 @@ def markdown(self, md: MetaMarkdown): ) if self.meta.summary is not None: - self.meta.summary = md.convert("\n".join(self.meta.summary)) + self.meta.summary = md.convert("\n".join(self.meta.summary), context=self) elif paragraph := PARA_CAPTURE_RE.search(self.doc): # If there is no stand-alone webpage for this item, e.g. # an internal routine, make the whole doc blob appear, @@ -421,7 +436,7 @@ def markdown(self, md: MetaMarkdown): self.meta.summary = "" if self.meta.summary.strip() != self.doc.strip(): - self.meta.summary += f'Read more…' + self.meta.summary += f'Read more…' if self.obj in ["proc", "type", "program"] and self.meta.source: obj = getattr(self, "proctype", self.obj).lower() @@ -1517,6 +1532,7 @@ def __init__( self.path = filepath.strip() self.name = os.path.basename(self.path) self.settings = settings + self.base_url = pathlib.Path(self.settings.project_url) self.fixed = fixed self.parent: Optional[FortranContainer] = None self.modules: List[FortranModule] = [] @@ -2221,6 +2237,7 @@ def __init__( self.parent = parent.parent self.parobj = self.parent.obj if self.parent else None self.settings = parent.settings + self.base_url = pathlib.Path(self.settings.project_url) self.visible = parent.visible self.num_lines = parent.num_lines self.doc_list = doc_list @@ -2250,6 +2267,7 @@ def __init__(self, name, parent, source=None): self.parobj = self.parent.obj self.display = self.parent.display self.settings = self.parent.settings + self.base_url = pathlib.Path(self.settings.project_url) self.doc_list = read_docstring(source, self.settings.docmark) if source else [] self.hierarchy = self._make_hierarchy() self.read_metadata() @@ -2292,6 +2310,9 @@ def __init__( else: self.parobj = None self.settings = None + self.base_url = pathlib.Path( + self.settings.project_url if self.settings else "." + ) self.obj = type(self).__name__[7:].lower() self.attribs = copy.copy(attribs) self.intent = intent @@ -2520,6 +2541,9 @@ def __init__(self, name, parent=None, inherited_permission=None): else: self.parobj = None self.settings = None + self.base_url = pathlib.Path( + self.settings.project_url if self.settings else "." + ) self.name = name self.procedure = None self.doc_list = [] @@ -2712,6 +2736,7 @@ def __init__(self, filename: PathLike, settings: ProjectSettings): self.parent = None self.hierarchy = [] self.settings = settings + self.base_url = self.settings.project_url self.num_lines = 0 filename = pathlib.Path(filename) extra_filetypes = settings.extra_filetypes[str(filename.suffix)[1:]] @@ -3093,10 +3118,6 @@ def parse_type( return ParsedType(vartype, rest, kind=kind) -def set_base_url(url: str) -> None: - FortranBase.base_url = url - - def get_mod_procs( source: FortranReader, names: str, parent: FortranInterface ) -> List[FortranModuleProcedureReference]: diff --git a/ford/templates/absint_list.html b/ford/templates/absint_list.html index 95dc5105..6cfb00c5 100644 --- a/ford/templates/absint_list.html +++ b/ford/templates/absint_list.html @@ -1,3 +1,5 @@ + + {% extends "base.html" %} {% block title %} All Abstract Interfaces – {{ project }} @@ -11,7 +13,11 @@

Abstract Interfaces

Abstract InterfaceLocationDescription {% for absint in project.absinterfaces|sort(attribute='name') %} - {{ absint }}{{ absint.parent }}{{ absint.procedure | meta('summary') }} + + {{ absint | relurl(page_url) }} + {{ absint.parent | relurl(page_url) }} + {{ absint.procedure | meta('summary') }} + {% endfor %} diff --git a/ford/templates/base.html b/ford/templates/base.html index cf355c77..b44124d5 100644 --- a/ford/templates/base.html +++ b/ford/templates/base.html @@ -45,7 +45,7 @@ diff --git a/ford/templates/block_page.html b/ford/templates/block_page.html index 6d163c7e..14f46e1a 100644 --- a/ford/templates/block_page.html +++ b/ford/templates/block_page.html @@ -1,3 +1,5 @@ + + {% extends "base.html" %} {% block title %}{{ blockdat.name|striptags }} – {{ project }}{% endblock title %} {% block body %} diff --git a/ford/templates/file_list.html b/ford/templates/file_list.html index 34986c25..ddef3167 100644 --- a/ford/templates/file_list.html +++ b/ford/templates/file_list.html @@ -1,3 +1,5 @@ + + {% extends "base.html" %} {% block title %} All Files – {{ project }} @@ -10,7 +12,10 @@

Source Files

FileDescription {% for src in project.allfiles|sort(attribute='name') %} - {{ src }}{{ src | meta('summary') }} + + {{ src | relurl(page_url) }} + {{ src | meta('summary') }} + {% endfor %} {{ project.filegraph }} diff --git a/ford/templates/file_page.html b/ford/templates/file_page.html index 961dd5d9..7fc7d6e5 100644 --- a/ford/templates/file_page.html +++ b/ford/templates/file_page.html @@ -1,3 +1,5 @@ + + {% extends "base.html" %}s {% block title %}{{ src.name }} – {{ project }}{% endblock title %} {% block body %} diff --git a/ford/templates/genint_page.html b/ford/templates/genint_page.html index ec5da5eb..e41d619d 100644 --- a/ford/templates/genint_page.html +++ b/ford/templates/genint_page.html @@ -1,3 +1,5 @@ + + {% extends "base.html" %} {% block title %}{{ interface.name }} – {{ project }}{% endblock title %} {% block body %} diff --git a/ford/templates/index.html b/ford/templates/index.html index e00d0b08..aaf924e4 100644 --- a/ford/templates/index.html +++ b/ford/templates/index.html @@ -94,7 +94,7 @@

{{ author }}

Source Files

@@ -111,7 +111,7 @@

Source Files

Modules

@@ -128,7 +128,7 @@

Modules

Procedures

@@ -145,7 +145,7 @@

Procedures

Derived Types

diff --git a/ford/templates/info_page.html b/ford/templates/info_page.html index 5b27a948..4fbc8071 100644 --- a/ford/templates/info_page.html +++ b/ford/templates/info_page.html @@ -1,3 +1,5 @@ + + {% extends "base.html" %} {% block title %}{{ page.title }} – {{ project }}{% endblock title %} {% block body %} @@ -19,7 +21,7 @@

{{ page.title }}