Repository: dev-cafe/parselglossy

Issue:
Repeated keywords in the autogenerated documentation
* parselglossy version:  b28084a
* Python version: 3.7.0
* Operating System: Ubuntu-18.10

### What I Did
The following `template.yml`
```
keywords:
  - name: foo
    type: int
    docstring: foo
  - name: bar
    type: int
    docstring: bar
  - name: baz
    type: int
    docstring: baz
```
is documented using the CLI command `parselglossy doc template.yml` to give:
```
**Keywords**
 :foo: foo

  **Type** ``int``

**Keywords**
 :foo: foo

  **Type** ``int``

 :bar: bar

  **Type** ``int``

**Keywords**
 :foo: foo

  **Type** ``int``

 :bar: bar

  **Type** ``int``

 :baz: baz

  **Type** ``int``

```



Discussion:

Patch:
diff --git a/parselglossy/documentation.py b/parselglossy/documentation.py
index f101cb7..0b8ed79 100644
--- a/parselglossy/documentation.py
+++ b/parselglossy/documentation.py
@@ -51,14 +51,19 @@ def documentation_generator(
     """
 
     comment = (
+        ".. raw:: html\n\n"
+        "    <style> .red {color:#aa0060; font-weight:bold; font-size:18px} </style>\n\n"  # noqa: E501
+        ".. role:: red\n\n"
         ".. This documentation was autogenerated using parselglossy."
         " Editing by hand is not recommended.\n"
     )
 
     header_fmt = (
         "{comment:s}\n{markup:s}\n{header:s}\n{markup:s}\n\n"
-        "Keywords without a default value are **required**.\n"
-        "Sections where all keywords have a default value can be omitted.\n"
+        "- Keywords without a default value are **required**.\n"
+        "- Default values are either explicit or computed from the value of other keywords in the input.\n"  # noqa: E501
+        "- Sections where all keywords have a default value can be omitted.\n"
+        "- Predicates, if present, are the functions run to validate user input.\n"
     )
 
     docs = rec_documentation_generator(template=template)
@@ -78,14 +83,26 @@ def document_keyword(keyword: JSONDict) -> str:
   **Type** ``{2:s}``
 """
 
-    doc = kw_fmt.format(keyword["name"], keyword["docstring"], keyword["type"])
+    doc = kw_fmt.format(
+        keyword["name"], keyword["docstring"].replace("\n", " "), keyword["type"]
+    )
 
     if "default" in keyword.keys():
         doc += """
-  **Default** {}""".format(
+  **Default** ``{}``
+""".format(
             keyword["default"]
         )
 
+    if "predicates" in keyword.keys():
+        preds = "\n    ".join(("- ``{}``".format(x) for x in keyword["predicates"]))
+        doc += """
+  **Predicates**
+    {}
+""".format(
+            preds
+        )
+
     return doc
 
 
@@ -106,20 +123,20 @@ def rec_documentation_generator(template, *, level: int = 0) -> str:
 
     keywords = template["keywords"] if "keywords" in template.keys() else []
     if keywords:
-        doc = "\n**Keywords**"
+        docs.append(indent("\n:red:`Keywords`", level))
+
     for k in keywords:
-        doc += document_keyword(k)
+        doc = document_keyword(k)
         docs.extend(indent(doc, level))
 
     sections = template["sections"] if "sections" in template.keys() else []
     if sections:
-        doc = "\n" if level == 0 else "\n\n"
-        doc += "**Sections**"
+        docs.append(indent("\n:red:`Sections`", level))
         fmt = r"""
  :{0:s}: {1:s}
 """
         for s in sections:
-            doc += fmt.format(s["name"], s["docstring"])
+            doc = fmt.format(s["name"], s["docstring"].replace("\n", " "))
             doc += rec_documentation_generator(s, level=level + 1)
             docs.extend(indent(doc, level))
 


Test patch:
diff --git a/tests/api/docs_template.yml b/tests/api/docs_template.yml
index 9966f26..2425855 100644
--- a/tests/api/docs_template.yml
+++ b/tests/api/docs_template.yml
@@ -1,20 +1,60 @@
 keywords:
-- docstring: Title of the calculation.
+- docstring: |
+    Title of the calculation. I had to write an extremely long documentation
+    string for this one, as it is extremely important to let you know that this
+    keyword will define the title of the calculation and, not having a default,
+    you will be required to set it!
   name: title
   type: str
+- docstring: Some integer
+  name: an_integer
+  type: int
+  default: 15
+- docstring: A list of floats
+  name: float_list
+  type: List[float]
+  default: [1.0, 2.0, 3.0]
+  predicates:
+    - "len(value) < 10"
+    - "max(value) < user['foo']['fooffa]['another_float']"
 sections:
 - docstring: brilliant
+  name: foo
   keywords:
   - default: Wham! Bang! Pow! Let's Rock Out!
     docstring: Title of the calculation.
     name: tragic
     type: str
-  name: foo
+  - name: a_float
+    type: float
+    docstring: A floating point number
+    predicates:
+      - "value < 35.0"
+      - "value in user['float_list']"
   sections:
   - docstring: A ba-bar section
+    name: Bar
     keywords:
     - default: Bobson Dugnutt
       docstring: Title of the calculation.
       name: amazing
       type: str
-    name: Bar
+    - name: coolio
+      type: bool
+      docstring: A cool bool
+      default: True
+- docstring: |
+    Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec luctus elit
+    ut posuere dictum. Proin ipsum libero, maximus vitae placerat a, bibendum
+    viverra ante.
+  name: fooffa
+  keywords:
+  - name: dwigt
+    docstring: An unusual name
+    type: str
+    predicates:
+      - "len(value) < 80"
+  - name: another_float
+    type: float
+    docstring: Another floating point number
+    default: "user['foo']['a_float'] * 2"
diff --git a/tests/cli/docs_template.yml b/tests/cli/docs_template.yml
index 9966f26..2425855 100644
--- a/tests/cli/docs_template.yml
+++ b/tests/cli/docs_template.yml
@@ -1,20 +1,60 @@
 keywords:
-- docstring: Title of the calculation.
+- docstring: |
+    Title of the calculation. I had to write an extremely long documentation
+    string for this one, as it is extremely important to let you know that this
+    keyword will define the title of the calculation and, not having a default,
+    you will be required to set it!
   name: title
   type: str
+- docstring: Some integer
+  name: an_integer
+  type: int
+  default: 15
+- docstring: A list of floats
+  name: float_list
+  type: List[float]
+  default: [1.0, 2.0, 3.0]
+  predicates:
+    - "len(value) < 10"
+    - "max(value) < user['foo']['fooffa]['another_float']"
 sections:
 - docstring: brilliant
+  name: foo
   keywords:
   - default: Wham! Bang! Pow! Let's Rock Out!
     docstring: Title of the calculation.
     name: tragic
     type: str
-  name: foo
+  - name: a_float
+    type: float
+    docstring: A floating point number
+    predicates:
+      - "value < 35.0"
+      - "value in user['float_list']"
   sections:
   - docstring: A ba-bar section
+    name: Bar
     keywords:
     - default: Bobson Dugnutt
       docstring: Title of the calculation.
       name: amazing
       type: str
-    name: Bar
+    - name: coolio
+      type: bool
+      docstring: A cool bool
+      default: True
+- docstring: |
+    Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec luctus elit
+    ut posuere dictum. Proin ipsum libero, maximus vitae placerat a, bibendum
+    viverra ante.
+  name: fooffa
+  keywords:
+  - name: dwigt
+    docstring: An unusual name
+    type: str
+    predicates:
+      - "len(value) < 80"
+  - name: another_float
+    type: float
+    docstring: Another floating point number
+    default: "user['foo']['a_float'] * 2"
diff --git a/tests/ref/dwigt.rst b/tests/ref/dwigt.rst
index 6d92abd..86670c0 100644
--- a/tests/ref/dwigt.rst
+++ b/tests/ref/dwigt.rst
@@ -1,33 +1,88 @@
+.. raw:: html
+
+    <style> .red {color:#aa0060; font-weight:bold; font-size:18px} </style>
+
+.. role:: red
+
 .. This documentation was autogenerated using parselglossy. Editing by hand is not recommended.
 
 ==========================================
 Dwigt Rortugal's guide to input parameters
 ==========================================
 
-Keywords without a default value are **required**.
-Sections where all keywords have a default value can be omitted.
+- Keywords without a default value are **required**.
+- Default values are either explicit or computed from the value of other keywords in the input.
+- Sections where all keywords have a default value can be omitted.
+- Predicates, if present, are the functions run to validate user input.
 
-**Keywords**
- :title: Title of the calculation.
+:red:`Keywords`
+ :title: Title of the calculation. I had to write an extremely long documentation string for this one, as it is extremely important to let you know that this keyword will define the title of the calculation and, not having a default, you will be required to set it! 
 
   **Type** ``str``
 
-**Sections**
+ :an_integer: Some integer
+
+  **Type** ``int``
+
+  **Default** ``15``
+
+ :float_list: A list of floats
+
+  **Type** ``List[float]``
+
+  **Default** ``[1.0, 2.0, 3.0]``
+
+  **Predicates**
+    - ``len(value) < 10``
+    - ``max(value) < user['foo']['fooffa]['another_float']``
+
+:red:`Sections`
  :foo: brilliant
 
-  **Keywords**
+  :red:`Keywords`
    :tragic: Title of the calculation.
   
     **Type** ``str``
   
-    **Default** Wham! Bang! Pow! Let's Rock Out!
+    **Default** ``Wham! Bang! Pow! Let's Rock Out!``
+  
+   :a_float: A floating point number
+  
+    **Type** ``float``
   
-  **Sections**
+    **Predicates**
+      - ``value < 35.0``
+      - ``value in user['float_list']``
+  
+  :red:`Sections`
    :Bar: A ba-bar section
   
-      **Keywords**
+      :red:`Keywords`
        :amazing: Title of the calculation.
       
         **Type** ``str``
       
-        **Default** Bobson Dugnutt
\ No newline at end of file
+        **Default** ``Bobson Dugnutt``
+      
+       :coolio: A cool bool
+      
+        **Type** ``bool``
+      
+        **Default** ``True``
+      
+ :fooffa: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec luctus elit ut posuere dictum. Proin ipsum libero, maximus vitae placerat a, bibendum viverra ante. 
+
+  :red:`Keywords`
+   :dwigt: An unusual name
+  
+    **Type** ``str``
+  
+    **Predicates**
+      - ``len(value) < 80``
+  
+   :another_float: Another floating point number
+  
+    **Type** ``float``
+  
+    **Default** ``user['foo']['a_float'] * 2``
+  
\ No newline at end of file
diff --git a/tests/ref/input.rst b/tests/ref/input.rst
index 6217605..a5f9cea 100644
--- a/tests/ref/input.rst
+++ b/tests/ref/input.rst
@@ -1,33 +1,88 @@
+.. raw:: html
+
+    <style> .red {color:#aa0060; font-weight:bold; font-size:18px} </style>
+
+.. role:: red
+
 .. This documentation was autogenerated using parselglossy. Editing by hand is not recommended.
 
 ================
 Input parameters
 ================
 
-Keywords without a default value are **required**.
-Sections where all keywords have a default value can be omitted.
+- Keywords without a default value are **required**.
+- Default values are either explicit or computed from the value of other keywords in the input.
+- Sections where all keywords have a default value can be omitted.
+- Predicates, if present, are the functions run to validate user input.
 
-**Keywords**
- :title: Title of the calculation.
+:red:`Keywords`
+ :title: Title of the calculation. I had to write an extremely long documentation string for this one, as it is extremely important to let you know that this keyword will define the title of the calculation and, not having a default, you will be required to set it! 
 
   **Type** ``str``
 
-**Sections**
+ :an_integer: Some integer
+
+  **Type** ``int``
+
+  **Default** ``15``
+
+ :float_list: A list of floats
+
+  **Type** ``List[float]``
+
+  **Default** ``[1.0, 2.0, 3.0]``
+
+  **Predicates**
+    - ``len(value) < 10``
+    - ``max(value) < user['foo']['fooffa]['another_float']``
+
+:red:`Sections`
  :foo: brilliant
 
-  **Keywords**
+  :red:`Keywords`
    :tragic: Title of the calculation.
   
     **Type** ``str``
   
-    **Default** Wham! Bang! Pow! Let's Rock Out!
+    **Default** ``Wham! Bang! Pow! Let's Rock Out!``
+  
+   :a_float: A floating point number
+  
+    **Type** ``float``
   
-  **Sections**
+    **Predicates**
+      - ``value < 35.0``
+      - ``value in user['float_list']``
+  
+  :red:`Sections`
    :Bar: A ba-bar section
   
-      **Keywords**
+      :red:`Keywords`
        :amazing: Title of the calculation.
       
         **Type** ``str``
       
-        **Default** Bobson Dugnutt
\ No newline at end of file
+        **Default** ``Bobson Dugnutt``
+      
+       :coolio: A cool bool
+      
+        **Type** ``bool``
+      
+        **Default** ``True``
+      
+ :fooffa: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec luctus elit ut posuere dictum. Proin ipsum libero, maximus vitae placerat a, bibendum viverra ante. 
+
+  :red:`Keywords`
+   :dwigt: An unusual name
+  
+    **Type** ``str``
+  
+    **Predicates**
+      - ``len(value) < 80``
+  
+   :another_float: Another floating point number
+  
+    **Type** ``float``
+  
+    **Default** ``user['foo']['a_float'] * 2``
+  
\ No newline at end of file
diff --git a/tests/ref/simple.rst b/tests/ref/simple.rst
new file mode 100644
index 0000000..fab7a8c
--- /dev/null
+++ b/tests/ref/simple.rst
@@ -0,0 +1,42 @@
+.. raw:: html
+
+    <style> .red {color:#aa0060; font-weight:bold; font-size:18px} </style>
+
+.. role:: red
+
+.. This documentation was autogenerated using parselglossy. Editing by hand is not recommended.
+
+================
+Input parameters
+================
+
+- Keywords without a default value are **required**.
+- Default values are either explicit or computed from the value of other keywords in the input.
+- Sections where all keywords have a default value can be omitted.
+- Predicates, if present, are the functions run to validate user input.
+
+:red:`Keywords`
+ :title: Title of the calculation.
+
+  **Type** ``str``
+
+:red:`Sections`
+ :foo: brilliant
+
+  :red:`Keywords`
+   :tragic: Title of the calculation.
+  
+    **Type** ``str``
+  
+    **Default** ``Wham! Bang! Pow! Let's Rock Out!``
+  
+  :red:`Sections`
+   :Bar: A ba-bar section
+  
+      :red:`Keywords`
+       :amazing: Title of the calculation.
+      
+        **Type** ``str``
+      
+        **Default** ``Bobson Dugnutt``
+      
\ No newline at end of file
diff --git a/tests/test_documentation.py b/tests/test_documentation.py
index f8d3aca..fcbb12e 100644
--- a/tests/test_documentation.py
+++ b/tests/test_documentation.py
@@ -71,7 +71,7 @@ def template():
 
 
 def test_documentation(template):
-    doc_ref = Path(__file__).parent / Path("ref/input.rst")
+    doc_ref = Path(__file__).parent / Path("ref/simple.rst")
     with doc_ref.open("r") as ref:
         stuff = ref.read().rstrip("\n")