Skip to content

Commit

Permalink
Merge pull request #526 from r-o-b-e-r-t-o/issue_390
Browse files Browse the repository at this point in the history
Adds Permalink feature.
Fixes #390
  • Loading branch information
danwos authored Apr 26, 2022
2 parents d40897e + abe139b commit e260939
Show file tree
Hide file tree
Showing 10 changed files with 227 additions and 16 deletions.
4 changes: 3 additions & 1 deletion AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,6 @@ Philip Partsch <[email protected]>

David Le Nir <[email protected]>

Baran Barış Yıldızlı <[email protected]>
Baran Barış Yıldızlı <[email protected]>

Roberto Rötting <[email protected]>
2 changes: 2 additions & 0 deletions docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ License
-----
:Released: under development

* Improvement: Add permanent link layout function.
(`#390 <https://github.com/useblocks/sphinxcontrib-needs/issues/390>`_)
* Bugfix: :ref:`needextract` not correctly rendering nested :ref:`need`s.
(`#329 <https://github.com/useblocks/sphinxcontrib-needs/issues/329>`_)

Expand Down
15 changes: 14 additions & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -298,7 +298,17 @@
"meta": ['**status**: <<meta("status")>>', '**author**: <<meta("author")>>'],
"side": ['<<image("_images/{{author}}.png", align="center")>>'],
},
}
},
"permalink_example": {
"grid": "simple",
"layout": {
"head": [
'<<meta("type_name")>>: **<<meta("title")>>** <<meta_id()>> <<permalink()>> <<collapse_button("meta", '
'collapsed="icon:arrow-down-circle", visible="icon:arrow-right-circle", initial=False)>> '
],
"meta": ["<<meta_all(no_links=True)>>", "<<meta_links_all()>>"],
},
},
}

needs_service_all_data = True
Expand Down Expand Up @@ -336,6 +346,9 @@
},
}

# build needs.json to make permalinks work
needs_build_json = True

# Get and maybe set Github credentials for services.
# This is needed as the rate limit for not authenticated users is too low for the amount of requests we
# need to perform for this documentation
Expand Down
39 changes: 39 additions & 0 deletions docs/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1498,6 +1498,45 @@ Default: False
The created ``needs.json`` file gets stored in the ``outdir`` of the current builder.
So if ``html`` is used as builder, the final location is e.g. ``_build/html/needs.json``.
.. _needs_permalink_file:
needs_permalink_file
~~~~~~~~~~~~~~~~~~~~
The option specifies the name of the permalink html file,
which will be copied to the html build directory during build.
The permalink web site will load a ``needs.json`` file as specified
by :ref:`needs_permalink_data` and re-direct the web browser to the html document
of the need, which is specified by appending the need ID as a query
parameter, e.g., ``http://localhost:8000/permalink.html?id=REQ_4711``.
Example:
.. code-block:: python
needs_permalink_file = "my_permalink.html"
Results in a file ``my_permalink.html`` in the
html build directory.
If this directory is served on ``localhost:8000``, then the file will be
available at ``http://localhost:8000/my_permalink.html``.
Default value: ``permalink.html``
.. _needs_permalink_data:
needs_permalink_data
~~~~~~~~~~~~~~~~~~~~
This options sets the location of a ``needs.json`` file.
This file is used to create permanent links for needs as described
in :ref:`needs_permalink_file`.
The path can be a relative path (relative to the permalink html file),
an absolute path (on the web server) or an URL.
Default value: ``needs.json``
Removed options
---------------
Expand Down
17 changes: 17 additions & 0 deletions docs/examples/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,14 @@ Different need layouts and styles
This example uses the value from **author** to reference an image.
See :ref:`layouts_styles` for the complete explanation.

.. req:: A requirement with a permalink
:id: EX_REQ_5
:tags: example
:status: open
:layout: permalink_example

This is like a normal requirement looks like but additionally a permalink icon is shown next to the ID.

Used rst-code for all three examples:

.. code-block:: rst
Expand Down Expand Up @@ -84,6 +92,15 @@ Used rst-code for all three examples:
This example uses the value from **author** to reference an image.
See :ref:`layouts_styles` for the complete explanation.
.. req:: A requirement with a permalink
:id: EX_REQ_5
:tags: example
:status: open
:layout: permalink_example
This is like a normal requirement looks like but additionally
a permalink icon is shown next to the ID.
Referencing and filtering needs
-------------------------------
.. req:: My first requirement
Expand Down
3 changes: 3 additions & 0 deletions docs/layout_styles.rst
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,7 @@ Available layout functions are:
* :func:`meta_id <sphinxcontrib.needs.layout.LayoutHandler.meta_id>`
* :func:`image <sphinxcontrib.needs.layout.LayoutHandler.image>`
* :func:`link <sphinxcontrib.needs.layout.LayoutHandler.link>`
* :func:`permalink <sphinxcontrib.needs.layout.LayoutHandler.permalink>`
* :func:`collapse_button <sphinxcontrib.needs.layout.LayoutHandler.collapse_button>`

.. autofunction:: sphinxcontrib.needs.layout.LayoutHandler.meta(name, prefix=None, show_empty=False)
Expand All @@ -378,6 +379,8 @@ Available layout functions are:

.. autofunction:: sphinxcontrib.needs.layout.LayoutHandler.link(url, text=None, image_url=None, image_height=None, image_width=None)

.. autofunction:: sphinxcontrib.needs.layout.LayoutHandler.permalink(image_url=None, image_height=None, image_width=None, text=None)

.. autofunction:: sphinxcontrib.needs.layout.LayoutHandler.collapse_button(target='meta', collapsed='Show', visible='Close', initial=False)

.. _styles:
Expand Down
24 changes: 24 additions & 0 deletions sphinxcontrib/needs/environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from typing import Iterable

import sphinx
from jinja2 import Environment, PackageLoader, select_autoescape
from pkg_resources import parse_version
from sphinx.application import Sphinx
from sphinx.util.console import brown
Expand Down Expand Up @@ -194,3 +195,26 @@ def install_lib_static_files(app: Sphinx, env):
lib_path = Path("sphinx-needs") / "libs" / "html"
for f in ["datatables.min.js", "datatables_loader.js", "datatables.min.css", "sphinx_needs_collapse.js"]:
safe_add_file(lib_path / f, app)


def install_permalink_file(app: Sphinx, env):
"""
Creates permalink.html in build dir
:param app:
:param env:
:return:
"""
# Do not copy static_files for our "needs" builder
if app.builder.name == "needs":
return

# load jinja template
jinja_env = Environment(loader=PackageLoader("sphinxcontrib.needs"), autoescape=select_autoescape())
template = jinja_env.get_template("permalink.html")

# save file to build dir
out_file = Path(app.builder.outdir) / Path(env.config.needs_permalink_file).name
with open(out_file, "w", encoding="utf-8") as f:
f.write(
template.render(permalink_file=env.config.needs_permalink_file, needs_file=env.config.needs_permalink_data)
)
63 changes: 49 additions & 14 deletions sphinxcontrib/needs/layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,7 @@ def __init__(self, app, need, layout, node, style=None, fromdocname=None):
"image": self.image,
"link": self.link,
"collapse_button": self.collapse_button,
"permalink": self.permalink,
}

# Prepare string_links dict, so that regex and templates get not recompiled too often.
Expand Down Expand Up @@ -767,7 +768,12 @@ def image(self, url, height=None, width=None, align=None, no_link=False, prefix=

url = file_path

image_node = nodes.image(url, classes=["needs_image"], **options)
if no_link:
classes = ["needs_image", "no-scaled-link"]
else:
classes = ["needs_image"]

image_node = nodes.image(url, classes=classes, **options)
image_node["candidates"] = {"*": url}
# image_node['candidates'] = '*'
image_node["uri"] = url
Expand All @@ -777,18 +783,6 @@ def image(self, url, height=None, width=None, align=None, no_link=False, prefix=
# Otherwise the images gets not copied to the later build-output location
self.app.env.images.add_file(self.need["docname"], url)

# Okay, this is really ugly.
# Sphinx does automatically wrap all images into a reference node, which links to the image file.
# See Bug: https://github.com/sphinx-doc/sphinx/issues/7032
# This behavior can only be avoided by not using width/height attributes or by adding our
# own reference node.
# We do last one here and set class to "no_link", which is later used by some javascript to avoid
# being clickable, so that the page does not "jump"
if no_link:
ref_node = nodes.reference("test", "", refuri="#", classes=["no_link"])
ref_node.append(image_node)
return ref_node

data_container.append(image_node)
return data_container

Expand Down Expand Up @@ -829,7 +823,7 @@ def link(self, url, text=None, image_url=None, image_height=None, image_width=No
link_node = nodes.reference(text, text, refuri=url)

if image_url:
image_node = self.image(image_url, image_height, image_width)
image_node = self.image(image_url, image_height, image_width, no_link=True)
link_node.append(image_node)

data_container.append(link_node)
Expand Down Expand Up @@ -887,6 +881,47 @@ def collapse_button(self, target="meta", collapsed="Show", visible="Close", init

return coll_container

def permalink(self, image_url=None, image_height=None, image_width=None, text=None, prefix=""):
"""
Shows a permanent link to the need.
Link can be a text, an image or both
:param image_url: image for an image link
:param image_height: None
:param image_width: None
:param text: text for a text link
:param prefix: Additional string infront of the string
:return:
Examples::
<<permalink()>>
<<permalink(image_url="icon:link")>>
<<permalink(text='¶')>>
"""

if image_url is None and text is None:
image_url = "icon:share-2"
image_width = "17px"

config = self.app.config
permalink = config.needs_permalink_file
id = self.need["id"]
docname = self.need["docname"]
permalink_url = ""
for _ in range(0, len(docname.split("/")) - 1):
permalink_url += "../"
permalink_url += permalink + "?id=" + id

return self.link(
url=permalink_url,
text=text,
image_url=image_url,
image_width=image_width,
image_height=image_height,
prefix=prefix,
)

def _grid_simple(self, colwidths, side_left, side_right, footer):
"""
Creates most "simple" grid layouts.
Expand Down
8 changes: 8 additions & 0 deletions sphinxcontrib/needs/needs.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@
)
from sphinxcontrib.needs.environment import (
install_lib_static_files,
install_permalink_file,
install_styles_static_files,
)
from sphinxcontrib.needs.external_needs import load_external_needs
Expand Down Expand Up @@ -226,6 +227,12 @@ def setup(app):

app.add_config_value("needs_build_json", False, "html", types=[bool])

# Permalink related config values.
# path to permalink.html; absolute path from web-root
app.add_config_value("needs_permalink_file", "permalink.html", "html")
# path to needs.json relative to permalink.html
app.add_config_value("needs_permalink_data", "needs.json", "html")

# Define nodes
app.add_node(Need, html=(html_visit, html_depart), latex=(latex_visit, latex_depart))
app.add_node(
Expand Down Expand Up @@ -320,6 +327,7 @@ def setup(app):
app.connect("build-finished", process_warnings)
app.connect("build-finished", build_needs_json)
app.connect("env-updated", install_lib_static_files)
app.connect("env-updated", install_permalink_file)

# Called during consistency check, which if after everything got read in.
# app.connect('env-check-consistency', process_warnings)
Expand Down
68 changes: 68 additions & 0 deletions sphinxcontrib/needs/templates/permalink.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>sphinx-needs permalink</title>

<script>
function loadJSON(filename, callback) {
var xobj = new XMLHttpRequest();
xobj.overrideMimeType('application/json');
xobj.open('GET', filename, true);
xobj.onreadystatechange = function () {
if (xobj.readyState == 4 && xobj.status == '200') {
callback(xobj.responseText);
}
};
xobj.send(null);
}

function main() {
loadJSON('{{ needs_file }}', function (response) {
const needs = JSON.parse(response);
const current_version = needs['current_version'];
const versions = needs['versions'];
const version = versions[current_version];
const needs_obj = version['needs'];

const id = getParameterByName('id');
var pathname = new URL(window.location.href).pathname;
pathname = pathname.substring(0, pathname.lastIndexOf('{{ permalink_file }}'));

const keys = Object.keys(needs_obj);

var docname = 'index';

keys.forEach((key, index) => {
if (key === id) {
const need = needs_obj[key];
docname = need['docname'];
return;
}
});

window.location.replace(pathname + docname + '.html#' + id);
});
}

function getParameterByName(name, url = window.location.href) {
name = name.replace(/[\[\]]/g, '\\$&');
var regex = new RegExp('[?&]' + name + '(=([^&#]*)|&|#|$)'),
results = regex.exec(url);
if (!results) return null;
if (!results[2]) return '';
return decodeURIComponent(results[2].replace(/\+/g, ' '));
}

window.addEventListener('DOMContentLoaded', main);
</script>

</head>

<body>
</body>

</html>

0 comments on commit e260939

Please sign in to comment.