Repository: mne-tools/mne-python

Issue:
Add non-breaking spaces between units
### Proposed documentation enhancement

There should be a non-breaking space between the number and its unit. I don't know if this is supported here, I would use either `&nbsp;` or `&thinsp;` (preferably the latter). This should be consistent throughout our documents.

e.g. 250&thinsp;ms


Discussion:
Is this issue still open?  I would like to fix this if still open.
Yes, this is still open. Have fun with it! :)
Can you give me a little direction? (or some resources)
*  Should I change across all  files? or specific type files? (python/html/rst)?
*  How do I check which space is used? (is there some tool or some way)

I am actually new in this project, and a little uninformed about variation of spaces. 

Probably I am asking for very beginner stuff. But I  an eager to contribute to this project.
Throughout our documentation, so that is the various `.rst` files in the `doc/` folder and the RST markup code in the `.py` files in the `examples` and `tutorials` folders, we have text explaining things. This text occasionally contains numbers with a unit. For example `100 ms`, or `23 µV`. At the moment, there is usually a normal, breaking, space between the number and the unit. Sometimes this causes awkward linebreaks. It would be better if we were to use a non-breaking space between them.

You can tell what type of space it is because anything other than a regular space probably needs to be written as a unicode directive in order for the documentation system to output a nonbreaking space. Question is how to exactly do this. This StackOverflow tread has some hints: https://stackoverflow.com/questions/11830242/how-can-i-make-a-non-breaking-space-in-restructuredtext

If it is true that we need some sort of custom RST substitution command to generate nonbreaking spaces, we might want to make a sphinx extension that defines a custom command for "units", so we could do something like 
```
:unit:`ms`
```
FYI one way it's done (using html escape `&nbsp`) is seen in this tutorial:

https://github.com/mne-tools/mne-python/blob/11bc09911a2dbb6b73a86b31d8140ccd8a93b1c3/tutorials/preprocessing/25_background_filtering.py#L728

Other approach (using a `\uXXXX` style escape, in this case for optional hyphens) is seen here:

https://github.com/mne-tools/mne-python/blob/11bc09911a2dbb6b73a86b31d8140ccd8a93b1c3/doc/conf.py#L880-L884

Yet another approach is the rST substitution syntax:

https://github.com/mne-tools/mne-python/blob/11bc09911a2dbb6b73a86b31d8140ccd8a93b1c3/doc/conf.py#L1074

`git grep "|ensp|"` will show you that one in-use.

All of those seem to me to make the rST source rather difficult to read. Making a custom rST interpreted text role (this is the ``:unit:`ms` `` style mentioned above) might be the nicest approach, especially if it were designed to take both the number and the unit, like this: ``:unit:`5 ms` `` and automatically replace the ordinary space with a non-breaking (possibly thin) space.
there's already some custom commands defined in `doc/sphinxext` you could use as example for how to create a new command.
:+1: for thin space!
Thank you for these resources.

After taking  some time for studying about `rst` and `sphinx`,I came up  with this custom role.

```
from docutils import nodes


def unit_role(name, rawtext, text, lineno, inliner, options={}, content=[]):
    try:
        value = float(text.split(' ')[0])
        if len(text.split(' ')) != 2:
            raise ValueError
    except ValueError:
        msg = inliner.reporter.error(
            'The structure should be a value(int/float) and a unit(str) separated by a normal space.Ex:`5 ms`', line=lineno)
        prb = inliner.problematic(rawtext, rawtext, msg)
        return [prb], [msg]
    node = nodes.Text("\u202F".join(text.split(' ')))
    return [node], []


def setup(app):
    app.add_role('unit', unit_role)
```

This results in `` :unit:`5 ms` ``. Does  this seem okay? 
this is a great start! Here's a proposed cleanup:

```py
from docutils import nodes
from mne.utils import _validate_type


def unit_role(name, rawtext, text, lineno, inliner, options={}, content=[]):
    parts = text.split()

    def pass_error_to_sphinx(rawtext, text, lineno, inliner):
        msg = inliner.reporter.error(
            'The :unit: role requires a space-separated number and unit; '
            f'got {text}', line=lineno)
        prb = inliner.problematic(rawtext, rawtext, msg)
        return [prb], [msg]

    # ensure only two parts
    if len(parts) != 2:
        return pass_error_to_sphinx(rawtext, text, lineno, inliner)
    # ensure first part is a number
    try:
        _validate_type(parts[0], 'numeric', 'Numeric part of :unit: role')
    except TypeError:
        return pass_error_to_sphinx(rawtext, text, lineno, inliner)
    # input is well-formatted: proceed
    node = nodes.Text('\u202F'.join(parts))
    return [node], []


def setup(app):
    app.add_role('unit', unit_role)
```

Can you open a PR that adds this role, and uses it instead of the `&nbsp` here:

https://github.com/mne-tools/mne-python/blob/11bc09911a2dbb6b73a86b31d8140ccd8a93b1c3/tutorials/preprocessing/25_background_filtering.py#L728

I don't think we test any of our sphinx extensions, but it ought to be fairly easy to test this.  @larsoner where would you suggest adding a test for our sphinx extensions?


> @larsoner where would you suggest adding a test for our sphinx extensions?

We don't currently test them. But we could add a `mne/utils/_sphinxext.py` and `mne/utils/tests/test_sphinxext.py` or so. I'm not sure how easy it will be to set up the infrastructure for it, the hoops we had to jump through in sphinx-gallery were not trivial. But we could figure it out
@drammock The cleanup has a little bug.  When we split the `text` into  `parts = text.split()`,   `parts[0]` is  also a  string. Thus, whether it is `1` or  `one`, it raises an error. I am not sure, if  `_validate_type` has a correct form of application here.

```py

from docutils import nodes


def unit_role(name, rawtext, text, lineno, inliner, options={}, content=[]):
    parts = text.split()

    def pass_error_to_sphinx(rawtext, text, lineno, inliner):
        msg = inliner.reporter.error(
            'The :unit: role requires a space-separated number and unit; '
            f'got {text}', line=lineno)
        prb = inliner.problematic(rawtext, rawtext, msg)
        return [prb], [msg]

    # ensure only two parts
    if len(parts) != 2:
        return pass_error_to_sphinx(rawtext, text, lineno, inliner)
    # ensure first part is a number
    try:
          value = float(parts[0])
    except ValueError:
        return pass_error_to_sphinx(rawtext, text, lineno, inliner)
    # input is well-formatted: proceed
    node = nodes.Text('\u202F'.join(parts))
    return [node], []


def setup(app):
    app.add_role('unit', unit_role)


```
I am seeing one  form of solution to  be this, with `ValueError`. Any suggestions?
Ah good point. Let's forget about using validate_type then

Resolving patch:
diff --git a/doc/conf.py b/doc/conf.py
--- a/doc/conf.py
+++ b/doc/conf.py
@@ -96,7 +96,8 @@
     'sphinxcontrib.bibtex',
     'sphinx_copybutton',
     'sphinx_design',
-    'sphinxcontrib.youtube'
+    'sphinxcontrib.youtube',
+    'unit_role',
 ]
 
 # Add any paths that contain templates here, relative to this directory.
diff --git a/doc/sphinxext/unit_role.py b/doc/sphinxext/unit_role.py
new file mode 100644
--- /dev/null
+++ b/doc/sphinxext/unit_role.py
@@ -0,0 +1,29 @@
+from docutils import nodes
+
+
+def unit_role(name, rawtext, text, lineno, inliner, options={}, content=[]):
+    parts = text.split()
+
+    def pass_error_to_sphinx(rawtext, text, lineno, inliner):
+        msg = inliner.reporter.error(
+            'The :unit: role requires a space-separated number and unit; '
+            f'got {text}', line=lineno)
+        prb = inliner.problematic(rawtext, rawtext, msg)
+        return [prb], [msg]
+
+    # ensure only two parts
+    if len(parts) != 2:
+        return pass_error_to_sphinx(rawtext, text, lineno, inliner)
+    # ensure first part is a number
+    try:
+        _ = float(parts[0])
+    except ValueError:
+        return pass_error_to_sphinx(rawtext, text, lineno, inliner)
+    # input is well-formatted: proceed
+    node = nodes.Text('\u202F'.join(parts))
+    return [node], []
+
+
+def setup(app):
+    app.add_role('unit', unit_role)
+    return
diff --git a/tutorials/preprocessing/25_background_filtering.py b/tutorials/preprocessing/25_background_filtering.py
--- a/tutorials/preprocessing/25_background_filtering.py
+++ b/tutorials/preprocessing/25_background_filtering.py
@@ -718,6 +718,7 @@ def plot_signal(x, offset):
     ax.plot(t, x_f, color='k', linestyle='--')
     ax.set(ylim=ylim, xlim=xlim, xticks=tticks,
            title=title, xlabel=xlabel, ylabel=ylabel)
+
 mne.viz.adjust_axes(axes)
 mne.viz.tight_layout()
 plt.show()
@@ -725,12 +726,12 @@ def plot_signal(x, offset):
 # %%
 # Similarly, in a P300 paradigm reported by
 # Kappenman & Luck (2010) :footcite:`KappenmanLuck2010`,
-# they found that applying a 1&nbsp;Hz high-pass decreased the probability of
-# finding a significant difference in the N100 response, likely because
-# the P300 response was smeared (and inverted) in time by the high-pass
-# filter such that it tended to cancel out the increased N100. However,
-# they nonetheless noted that some high-passing can still be useful to deal
-# with drifts in the data.
+# of they found that applying a :unit:`1 Hz` high-pass decreased the
+# probability finding a significant difference in the N100 response, likely
+# because the P300 response was smeared (and inverted) in time by the
+# high-pass filter such that it tended to cancel out the increased N100.
+# However, they nonetheless noted that some high-passing can still be useful
+# to deal with drifts in the data.
 #
 # Even though these papers generally advise a 0.1 Hz or lower frequency for
 # a high-pass, it is important to keep in mind (as most authors note) that


Test patch: