Index: rtemsqual/applconfig.py
===================================================================
--- rtemsqual/applconfig.py	(revision 22ec40f5ec8ba636f9dc5442c7025d3ffb0aba68)
+++ rtemsqual/applconfig.py	(revision 520ba1dd6db7eee5963128c02ab0ae51e77f610a)
@@ -189,17 +189,14 @@
 
 
-def _generate_content(group: Item, options: ItemMap) -> SphinxContent:
+def _generate_file(group: Item, options: ItemMap, target: str) -> None:
     content = SphinxContent()
     group.register_license_and_copyrights(content)
     content.add_header(group["appl-config-group-name"], level="=")
-    content.add_blank_line()
-    content.add_lines(group["appl-config-group-description"])
+    content.add(group["appl-config-group-description"])
     for item in sorted(options.values(), key=lambda x: x.uid):
         name = item["appl-config-option-name"]
         item.register_license_and_copyrights(content)
         content.add_index_entries([name] + item["appl-config-option-index"])
-        content.add_blank_line()
         content.add_label(name)
-        content.add_blank_line()
         content.add_header(name, level="-")
         content.add_definition_item("CONSTANT:", f"``{name}``")
@@ -211,5 +208,5 @@
         _generate_notes(content, item["appl-config-option-notes"])
     content.add_licence_and_copyrights()
-    return content
+    content.write(target)
 
 
@@ -231,4 +228,3 @@
         options = {}  # type: ItemMap
         _gather_options(group, options)
-        content = _generate_content(group, options)
-        content.write(group_config["target"])
+        _generate_file(group, options, group_config["target"])
Index: rtemsqual/content.py
===================================================================
--- rtemsqual/content.py	(revision 22ec40f5ec8ba636f9dc5442c7025d3ffb0aba68)
+++ rtemsqual/content.py	(revision 520ba1dd6db7eee5963128c02ab0ae51e77f610a)
@@ -25,9 +25,13 @@
 # POSSIBILITY OF SUCH DAMAGE.
 
+from contextlib import contextmanager
+import itertools
 import os
 import re
 import textwrap
-
-from typing import List, Union
+from typing import Callable, ContextManager, Iterator, List, Optional, Union
+
+AddContext = Callable[["Content"], ContextManager[None]]
+GenericContent = Union[str, List[str], "Content"]
 
 
@@ -120,37 +124,139 @@
 
 
-def _make_lines(lines: Union[str, List[str]]) -> List[str]:
-    if not isinstance(lines, list):
-        return lines.strip("\n").split("\n")
+def _make_lines(content: GenericContent) -> List[str]:
+    if isinstance(content, str):
+        return content.strip("\n").split("\n")
+    if isinstance(content, list):
+        return content
+    return content.lines
+
+
+def _indent(lines: List[str], indent: str,
+            empty_line_indent: str) -> List[str]:
+    if indent:
+        return [
+            indent + line if line else empty_line_indent + line
+            for line in lines
+        ]
     return lines
 
 
-def _make_list(value):
-    if not isinstance(value, list):
-        return [value]
-    return value
+@contextmanager
+def _add_context(_content: "Content") -> Iterator[None]:
+    yield
 
 
 class Content:
     """ This class builds content. """
+
+    # pylint: disable=too-many-instance-attributes
     def __init__(self, the_license):
-        self._content = ""
+        self._lines = []  # type: List[str]
         self._license = the_license
         self._copyrights = Copyrights()
-
-    def add(self, content: str) -> None:
-        """ Adds content to the content. """
-        self._content += content
+        self._gap = False
+        self._tab = "  "
+        self._indents = [""]
+        self._indent = ""
+        self._empty_line_indents = [""]
+        self._empty_line_indent = ""
+
+    def __str__(self):
+        return "\n".join(itertools.chain(self._lines, [""]))
+
+    @property
+    def lines(self) -> List[str]:
+        """ The lines. """
+        return self._lines
+
+    def append(self, content: GenericContent) -> None:
+        """ Appends the content. """
+        self._lines.extend(
+            _indent(_make_lines(content), self._indent,
+                    self._empty_line_indent))
+
+    def prepend(self, content: GenericContent) -> None:
+        """ Prepends the content. """
+        self._lines[0:0] = _indent(_make_lines(content), self._indent,
+                                   self._empty_line_indent)
+
+    def add(self,
+            content: Optional[GenericContent],
+            context: AddContext = _add_context) -> None:
+        """
+        Skips leading empty lines, adds a gap if needed, then adds the content.
+        """
+        if not content:
+            return
+        lines = _make_lines(content)
+        index = 0
+        for line in lines:
+            if line:
+                if self._gap:
+                    self._lines.extend(
+                        _indent([""], self._indent, self._empty_line_indent))
+                self._gap = True
+                with context(self):
+                    self._lines.extend(
+                        _indent(lines[index:], self._indent,
+                                self._empty_line_indent))
+                break
+            index += 1
+
+    @property
+    def gap(self) -> bool:
+        """
+        True if the next Content.add() adds a gap before the new content,
+        otherwise False.
+        """
+        return self._gap
+
+    @gap.setter
+    def gap(self, value: bool) -> None:
+        """ Sets the gap indicator for Content.add(). """
+        self._gap = value
+
+    def _update_indent(self) -> None:
+        self._indent = "".join(self._indents)
+        empty_line_indent = "".join(self._empty_line_indents)
+        if empty_line_indent.isspace():
+            self._empty_line_indent = ""
+        else:
+            self._empty_line_indent = empty_line_indent
+
+    def push_indent(self,
+                    indent: Optional[str] = None,
+                    empty_line_indent: Optional[str] = None) -> None:
+        """ Pushes an indent level. """
+        self._indents.append(indent if indent else self._tab)
+        self._empty_line_indents.append(
+            empty_line_indent if empty_line_indent else self._tab)
+        self._update_indent()
+
+    def pop_indent(self) -> None:
+        """ Pops an indent level. """
+        self._indents.pop()
+        self._empty_line_indents.pop()
+        self._update_indent()
+
+    @contextmanager
+    def indent(self,
+               indent: Optional[str] = None,
+               empty_line_indent: Optional[str] = None) -> Iterator[None]:
+        """ Opens an indent context. """
+        self.push_indent(indent, empty_line_indent)
+        yield
+        self.pop_indent()
+
+    def indent_lines(self, level: int) -> None:
+        """ Indents all lines by the specified indent level. """
+        prefix = level * self._tab
+        self._lines = [prefix + line if line else line for line in self._lines]
 
     def add_blank_line(self):
-        """ Adds a blank line to the content. """
-        self.add("\n")
-
-    @property
-    def content(self):
-        """ Returns the content. """
-        return self._content
-
-    def register_license(self, the_license):
+        """ Adds a blank line. """
+        self._lines.append("")
+
+    def register_license(self, the_license: str) -> None:
         """ Registers a licence for the content. """
         licenses = re.split(r"\s+OR\s+", the_license)
@@ -158,9 +264,9 @@
             raise ValueError(the_license)
 
-    def register_copyright(self, statement):
+    def register_copyright(self, statement: str) -> None:
         """ Registers a copyright statement for the content. """
         self._copyrights.register(statement)
 
-    def write(self, path):
+    def write(self, path: str) -> None:
         """ Writes the content to the file specified by the path. """
         directory = os.path.dirname(path)
@@ -168,5 +274,5 @@
             os.makedirs(directory, exist_ok=True)
         with open(path, "w+") as out:
-            out.write(self._content)
+            out.write(str(self))
 
 
@@ -175,56 +281,40 @@
     def __init__(self):
         super().__init__("CC-BY-SA-4.0")
-
-    def add_label(self, label):
-        """ Adds a label to the content. """
-        self._content += ".. _" + label.strip() + ":\n"
-
-    def add_header(self, name, level="="):
-        """ Adds a header to the content. """
+        self._tab = "    "
+
+    def add_label(self, label: str) -> None:
+        """ Adds a label. """
+        self.add(".. _" + label.strip() + ":")
+
+    def add_header(self, name, level="=") -> None:
+        """ Adds a header. """
         name = name.strip()
-        self._content += name + "\n" + level * len(name) + "\n"
-
-    def add_line(self, line, indent=0):
-        """ Adds a line to the content. """
-        if line:
-            self._content += indent * "    " + line + "\n"
-        else:
-            self._content += "\n"
-
-    def add_lines(self, lines, indent=0):
-        """ Adds a lines to the content. """
-        for line in _make_lines(lines):
-            self.add_line(line, indent)
-
-    def add_index_entries(self, entries):
+        self.add([name, level * len(name)])
+
+    def add_index_entries(self, entries) -> None:
         """ Adds a list of index entries the content. """
-        first = True
-        for entry in _make_list(entries):
-            if first:
-                first = False
-                self.add_blank_line()
-            self._content += ".. index:: " + entry + "\n"
-
-    def add_definition_item(self, name, lines, indent=0):
+        self.add([".. index:: " + entry for entry in _make_lines(entries)])
+
+    def add_definition_item(self, name, lines) -> None:
         """ Adds a definition item the content. """
-        first = True
-        for line in _make_lines(lines):
-            if first:
-                first = False
-                self.add_blank_line()
-                self.add_line(name, indent)
-            self.add_line(line, indent=indent + 1)
-
-    def add_licence_and_copyrights(self):
-        """
-        Adds a licence and copyright block to the content according to the
-        registered licenses and copyrights.
-        """
-        spdx = f".. SPDX-License-Identifier: {self._license}\n"
-        statements = "\n.. ".join(self._copyrights.get_statements())
+        @contextmanager
+        def _definition_item_context(content: Content) -> Iterator[None]:
+            content.append(name)
+            content.push_indent()
+            yield
+            content.pop_indent()
+
+        self.add(lines, _definition_item_context)
+
+    def add_licence_and_copyrights(self) -> None:
+        """
+        Adds a licence and copyright block according to the registered licenses
+        and copyrights.
+        """
+        statements = self._copyrights.get_statements()
         if statements:
-            self._content = f"{spdx}\n.. {statements}\n\n{self._content}"
-        else:
-            self._content = f"{spdx}\n{self._content}"
+            self.prepend("")
+            self.prepend([f".. {stm}" for stm in statements])
+        self.prepend([f".. SPDX-License-Identifier: {self._license}", ""])
 
 
@@ -256,32 +346,31 @@
 
 
-_BSD_2_CLAUSE_LICENSE = """
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions
- * are met:
- * 1. Redistributions of source code must retain the above copyright
- *    notice, this list of conditions and the following disclaimer.
- * 2. Redistributions in binary form must reproduce the above copyright
- *    notice, this list of conditions and the following disclaimer in the
- *    documentation and/or other materials provided with the distribution.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
- * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
- * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
- * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
- * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
- * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
- * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
- * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
- * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
- * POSSIBILITY OF SUCH DAMAGE.
- */
-"""
+_BSD_2_CLAUSE_LICENSE = """Redistribution and use in source and binary \
+forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+1. Redistributions of source code must retain the above copyright
+   notice, this list of conditions and the following disclaimer.
+2. Redistributions in binary form must reproduce the above copyright
+   notice, this list of conditions and the following disclaimer in the
+   documentation and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGE."""
 
 
 class CContent(Content):
     """ This class builds C content. """
+
+    # pylint: disable=too-many-public-methods
     def __init__(self):
         super().__init__("BSD-2-Clause")
@@ -289,40 +378,20 @@
     def add_spdx_license_identifier(self):
         """
-        Adds an SPDX License Identifier to the content according to the
-        registered licenses.
-        """
-        spdx = f"/* SPDX-License-Identifier: {self._license} */"
-        self.add(f"{spdx}\n{self._content}")
+        Adds an SPDX License Identifier according to the registered licenses.
+        """
+        self.prepend([f"/* SPDX-License-Identifier: {self._license} */", ""])
 
     def add_copyrights_and_licenses(self):
         """
-        Adds the copyrights and licenses to the content according to the
-        registered copyrights and licenses.
-        """
-        self.add("\n/*\n * ")
-        self.add("\n * ".join(self._copyrights.get_statements()))
-        self.add(_BSD_2_CLAUSE_LICENSE)
+        Adds the copyrights and licenses according to the registered copyrights
+        and licenses.
+        """
+        with self.comment_block():
+            self.add(self._copyrights.get_statements())
+            self.add(_BSD_2_CLAUSE_LICENSE)
 
     def add_have_config(self):
         """ Adds a guarded config.h include. """
-        self.add("""
-#ifdef HAVE_CONFIG_H
-#include "config.h"
-#endif
-""")
-
-    def add_lines(self, lines: Union[str, List[str]], indent: int = 0) -> None:
-        """ Adds the lines to the content. """
-        if lines:
-            sep = "\n" + indent * "  "
-            self.add(indent * "  " + sep.join(_make_lines(lines)) + "\n")
-
-    def add_line_block(self,
-                       lines: Union[str, List[str]],
-                       indent: int = 0) -> None:
-        """ Adds a block of lines to the content. """
-        if lines:
-            sep = "\n" + indent * "  "
-            self.add(sep + sep.join(_make_lines(lines)) + "\n")
+        self.add(["#ifdef HAVE_CONFIG_H", "#include \"config.h\"", "#endif"])
 
     def add_includes(self, includes: List[str], local: bool = False) -> None:
@@ -347,22 +416,119 @@
         left = "\"" if local else "<"
         right = "\"" if local else ">"
-        incs = [f"#include {left}{inc}{right}" for inc in includes]
-        self.add_line_block(sorted(set(incs), key=IncludeKey))
-
-    def add_comment_content(self,
-                            content: str,
-                            indent: int = 0,
-                            intro: str = "") -> None:
-        """ Adds comment content to the content. """
-        if content:
-            text = textwrap.TextWrapper()
-            text.drop_whitespace = True
-            sep = indent * "  " + " * "
-            text.initial_indent = sep + intro
-            text.subsequent_indent = sep + len(intro) * " "
-            self.add(indent * "  " + " *\n" + "\n".join(text.wrap(content)) +
-                     "\n")
-
-    def add_brief_description(self, description: str, indent: int = 0) -> None:
-        """ Adds a brief description to the content. """
-        self.add_comment_content(description, indent, intro="@brief ")
+        self.add([
+            f"#include {left}{inc}{right}"
+            for inc in sorted(set(includes), key=IncludeKey)
+        ])
+
+    def wrap(self, content: Optional[str], intro: str = "") -> List[str]:
+        """ Wraps a text. """
+        if not content:
+            return [""]
+        wrapper = textwrap.TextWrapper()
+        wrapper.drop_whitespace = True
+        wrapper.initial_indent = intro
+        wrapper.subsequent_indent = len(intro) * " "
+        wrapper.width = 79 - len(self._indent)
+        return wrapper.wrap(content)
+
+    def _open_comment_block(self, begin) -> None:
+        self.add(begin)
+        self.push_indent(" * ", " *")
+        self.gap = False
+
+    def open_comment_block(self) -> None:
+        """ Opens a comment block. """
+        self._open_comment_block("/*")
+
+    def open_doxygen_block(self) -> None:
+        """ Opens a Doxygen comment block. """
+        self._open_comment_block("/**")
+
+    def open_file_block(self) -> None:
+        """ Opens a Doxygen @file comment block. """
+        self._open_comment_block(["/**", " * @file"])
+        self.gap = True
+
+    def open_defgroup_block(self, identifier: str, name: str) -> None:
+        """ Opens a Doxygen @defgroup comment block. """
+        self._open_comment_block(["/**", f" * @defgroup {identifier} {name}"])
+        self.gap = True
+
+    def open_function_block(self, function: str) -> None:
+        """ Opens a Doxygen @fn comment block. """
+        self._open_comment_block(["/**", f" * @fn {function}"])
+        self.gap = True
+
+    def close_comment_block(self) -> None:
+        """ Closes a comment block. """
+        self.pop_indent()
+        self.append(" */")
+        self.gap = True
+
+    @contextmanager
+    def comment_block(self) -> Iterator[None]:
+        """ Opens a comment block context. """
+        self.open_comment_block()
+        yield
+        self.close_comment_block()
+
+    @contextmanager
+    def doxygen_block(self) -> Iterator[None]:
+        """ Opens a Doxygen comment block context. """
+        self.open_doxygen_block()
+        yield
+        self.close_comment_block()
+
+    @contextmanager
+    def file_block(self) -> Iterator[None]:
+        """ Opens a Doxygen @file comment block context. """
+        self.open_file_block()
+        yield
+        self.close_comment_block()
+
+    @contextmanager
+    def defgroup_block(self, identifier: str, name: str) -> Iterator[None]:
+        """ Opens a Doxygen @defgroup comment block context. """
+        self.open_defgroup_block(identifier, name)
+        yield
+        self.close_comment_block()
+
+    @contextmanager
+    def function_block(self, function: str) -> Iterator[None]:
+        """ Opens a Doxygen @fn comment block context. """
+        self.open_function_block(function)
+        yield
+        self.close_comment_block()
+
+    def add_brief_description(self, description: Optional[str]) -> None:
+        """ Adds a brief description. """
+        return self.add(self.wrap(description, intro="@brief "))
+
+    def add_ingroup(self, ingroups: List[str]) -> None:
+        """ Adds an ingroup comment block. """
+        self.add(["@ingroup " + ingroup for ingroup in sorted(set(ingroups))])
+
+    def add_group(self, identifier: str, name: str, ingroups: List[str],
+                  brief: Optional[str], description: Optional[str]) -> None:
+        # pylint: disable=too-many-arguments
+        """ Adds a group definition. """
+        with self.defgroup_block(identifier, name):
+            self.add_ingroup(ingroups)
+            self.add_brief_description(brief)
+            self.add(self.wrap(description))
+
+    @contextmanager
+    def header_guard(self, filename: str) -> Iterator[None]:
+        """ Opens a header guard context. """
+        filename = os.path.basename(filename)
+        guard = "_" + filename.replace(".", "_").upper()
+        self.add([f"#ifndef {guard}", f"#define {guard}"])
+        yield
+        self.add(f"#endif /* {guard} */")
+
+    @contextmanager
+    def extern_c(self) -> Iterator[None]:
+        """ Opens an extern "C" context. """
+        self.add(["#ifdef __cplusplus", "extern \"C\" {", "#endif"])
+        yield
+        self.add(["#ifdef __cplusplus", "}", "#endif"])
Index: rtemsqual/glossary.py
===================================================================
--- rtemsqual/glossary.py	(revision 22ec40f5ec8ba636f9dc5442c7025d3ffb0aba68)
+++ rtemsqual/glossary.py	(revision 520ba1dd6db7eee5963128c02ab0ae51e77f610a)
@@ -52,7 +52,7 @@
     content = SphinxContent()
     content.add_header("Glossary", level="*")
-    content.add_blank_line()
-    content.add_line(".. glossary::")
-    content.add_line(":sorted:", indent=1)
+    content.add(".. glossary::")
+    with content.indent():
+        content.append(":sorted:")
     macro_to_sphinx = MacroToSphinx()
     macro_to_sphinx.set_terms(terms)
@@ -61,5 +61,6 @@
         text = macro_to_sphinx.substitute(item["text"].strip())
         item.register_license_and_copyrights(content)
-        content.add_definition_item(item["glossary-term"], text, indent=1)
+        with content.indent():
+            content.add_definition_item(item["glossary-term"], text)
     content.add_licence_and_copyrights()
     return content
Index: rtemsqual/tests/test_content.py
===================================================================
--- rtemsqual/tests/test_content.py	(revision 520ba1dd6db7eee5963128c02ab0ae51e77f610a)
+++ rtemsqual/tests/test_content.py	(revision 520ba1dd6db7eee5963128c02ab0ae51e77f610a)
@@ -0,0 +1,117 @@
+# SPDX-License-Identifier: BSD-2-Clause
+""" Unit tests for the rtemsqual.content module. """
+
+# Copyright (C) 2020 embedded brains GmbH (http://www.embedded-brains.de)
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+# 1. Redistributions of source code must retain the above copyright
+#    notice, this list of conditions and the following disclaimer.
+# 2. Redistributions in binary form must reproduce the above copyright
+#    notice, this list of conditions and the following disclaimer in the
+#    documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+
+import os
+
+from rtemsqual.content import Content
+
+
+def test_append():
+    content = Content("BSD-2-Clause")
+    content.append("")
+    assert str(content) == """
+"""
+    content.append(["a", "b"])
+    assert str(content) == """
+a
+b
+"""
+    with content.indent():
+        content.append(["c", "d"])
+        assert str(content) == """
+a
+b
+  c
+  d
+"""
+
+
+def test_prepend():
+    content = Content("BSD-2-Clause")
+    content.prepend("")
+    assert str(content) == """
+"""
+    content.prepend(["a", "b"])
+    assert str(content) == """a
+b
+
+"""
+    with content.indent():
+        content.prepend(["c", "d"])
+        assert str(content) == """  c
+  d
+a
+b
+
+"""
+
+
+def test_add():
+    content = Content("BSD-2-Clause")
+    content.add("")
+    assert str(content) == ""
+    content.add("a")
+    assert str(content) == """a
+"""
+    content.add(["b", "c"])
+    assert str(content) == """a
+
+b
+c
+"""
+
+
+def test_add_blank_line():
+    content = Content("BSD-2-Clause")
+    content.add_blank_line()
+    assert str(content) == """
+"""
+
+
+def test_indent():
+    content = Content("BSD-2-Clause")
+    content.add_blank_line()
+    content.append("x")
+    content.indent_lines(3)
+    assert str(content) == """
+      x
+"""
+
+
+def test_write(tmpdir):
+    content = Content("BSD-2-Clause")
+    content.append("x")
+    path = os.path.join(tmpdir, "x", "y")
+    content.write(path)
+    with open(path, "r") as src:
+        assert src.read() == """x
+"""
+    tmpdir.chdir()
+    path = "z"
+    content.write(path)
+    with open(path, "r") as src:
+        assert src.read() == """x
+"""
Index: rtemsqual/tests/test_content_c.py
===================================================================
--- rtemsqual/tests/test_content_c.py	(revision 22ec40f5ec8ba636f9dc5442c7025d3ffb0aba68)
+++ rtemsqual/tests/test_content_c.py	(revision 520ba1dd6db7eee5963128c02ab0ae51e77f610a)
@@ -31,5 +31,13 @@
     content = CContent()
     content.add_have_config()
-    assert content.content == """
+    assert str(content) == """#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+"""
+    content.add_have_config()
+    assert str(content) == """#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
 #ifdef HAVE_CONFIG_H
 #include "config.h"
@@ -38,79 +46,67 @@
 
 
-def test_add_lines():
-    content = CContent()
-    content.add_lines("")
-    assert content.content == ""
-    content.add_lines("a\nb\n")
-    assert content.content == """a
-b
-"""
-    content.add_lines(["c", "d"], indent=1)
-    assert content.content == """a
-b
-  c
-  d
-"""
-
-
-def test_add_line_block():
-    content = CContent()
-    content.add_line_block("")
-    assert content.content == ""
-    content.add_line_block("a")
-    assert content.content == """
-a
-"""
-    content.add_line_block(["b", "c"])
-    assert content.content == """
-a
-
-b
-c
-"""
-
-
 def test_add_includes():
     content = CContent()
     content.add_includes([])
-    assert content.content == ""
+    assert str(content) == ""
     content = CContent()
     content.add_includes(["a", "a"])
-    assert content.content == """
-#include <a>
+    assert str(content) == """#include <a>
+"""
+    content.add_includes(["b"])
+    assert str(content) == """#include <a>
+
+#include <b>
 """
     content = CContent()
     content.add_includes(["c", "b"], local=True)
-    assert content.content == """
-#include "b"
+    assert str(content) == """#include "b"
 #include "c"
 """
     content = CContent()
     content.add_includes(["d/f", "d/e"])
-    assert content.content == """
-#include <d/e>
+    assert str(content) == """#include <d/e>
 #include <d/f>
 """
     content = CContent()
     content.add_includes(["h", "g/h"])
-    assert content.content == """
-#include <h>
+    assert str(content) == """#include <h>
 #include <g/h>
 """
     content = CContent()
     content.add_includes(["i/l/k", "i/j/k"])
-    assert content.content == """
-#include <i/j/k>
+    assert str(content) == """#include <i/j/k>
 #include <i/l/k>
 """
 
 
-def test_add_comment_content():
+def test_comment_block():
     content = CContent()
-    content.add_comment_content("")
-    assert content.content == ""
-    content.add_comment_content("a")
-    assert content.content == """ *
+    with content.comment_block():
+        assert not content.gap
+        content.add(content.wrap(""))
+        assert not content.gap
+        assert str(content) == """/*
+"""
+        content.add(content.wrap("a"))
+        assert content.gap
+        assert str(content) == """/*
  * a
+"""
+        content.add(content.wrap("b"))
+        assert content.gap
+        assert str(content) == """/*
+ * a
+ *
+ * b
+"""
+        content.gap = False
+        content.add(content.wrap("c"))
+        assert content.gap
+        assert str(content) == """/*
+ * a
+ *
+ * b
+ * c
 """
 
@@ -119,5 +115,6 @@
     content = CContent()
     content.add_brief_description("")
-    assert content.content == ""
+    assert str(content) == ""
+    content.gap = True
     content.add_brief_description(
         "THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT "
@@ -126,9 +123,8 @@
         "OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE "
         "DISCLAIMED.")
-    assert content.content == """ *
- * @brief THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
- *        CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
- *        INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
- *        MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
- *        DISCLAIMED.
+    assert str(content) == """
+@brief THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
+       IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+       TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+       PARTICULAR PURPOSE ARE DISCLAIMED.
 """
Index: rtemsqual/tests/test_content_sphinx.py
===================================================================
--- rtemsqual/tests/test_content_sphinx.py	(revision 22ec40f5ec8ba636f9dc5442c7025d3ffb0aba68)
+++ rtemsqual/tests/test_content_sphinx.py	(revision 520ba1dd6db7eee5963128c02ab0ae51e77f610a)
@@ -25,5 +25,4 @@
 # POSSIBILITY OF SUCH DAMAGE.
 
-import os
 import pytest
 
@@ -41,5 +40,6 @@
     sc = SphinxContent()
     sc.add_label("x")
-    assert ".. _x:\n" == sc.content
+    assert str(sc) == """.. _x:
+"""
 
 
@@ -47,31 +47,24 @@
     sc = SphinxContent()
     sc.add_header("x")
-    assert "x\n=\n" == sc.content
+    assert str(sc) == """x
+=
+"""
 
 
-def test_add_blank_line():
+def test_append():
     sc = SphinxContent()
-    sc.add_blank_line()
-    assert "\n" == sc.content
+    sc.append("x")
+    assert str(sc) == """x
+"""
+    with sc.indent():
+        sc.append("y")
+        assert str(sc) == """x
+    y
+"""
+        sc.append("")
+        assert str(sc) == """x
+    y
 
-
-def test_add_line():
-    sc = SphinxContent()
-    sc.add_line("x")
-    assert "x\n" == sc.content
-    sc.add_line("y", 1)
-    assert "x\n    y\n" == sc.content
-    sc.add_line("")
-    assert "x\n    y\n\n" == sc.content
-
-
-def test_add_lines():
-    sc = SphinxContent()
-    sc.add_lines("x")
-    assert sc.content == "x\n"
-    sc.add_lines("y", 1)
-    assert sc.content == "x\n    y\n"
-    sc.add_lines(["a", "b"])
-    assert sc.content == "x\n    y\na\nb\n"
+"""
 
 
@@ -79,7 +72,13 @@
     sc = SphinxContent()
     sc.add_index_entries(["x", "y"])
-    assert "\n.. index:: x\n.. index:: y\n" == sc.content
+    assert str(sc) == """.. index:: x
+.. index:: y
+"""
     sc.add_index_entries("z")
-    assert "\n.. index:: x\n.. index:: y\n\n.. index:: z\n" == sc.content
+    assert str(sc) == """.. index:: x
+.. index:: y
+
+.. index:: z
+"""
 
 
@@ -87,8 +86,13 @@
     sc = SphinxContent()
     sc.add_definition_item("x", ["y", "z"])
-    assert sc.content == "\nx\n    y\n    z\n"
+    assert str(sc) == """x
+    y
+    z
+"""
     sc = SphinxContent()
     sc.add_definition_item("a", "\n b\n")
-    assert sc.content == "\na\n     b\n"
+    assert str(sc) == """a
+     b
+"""
 
 
@@ -98,7 +102,9 @@
         sc.register_license("x")
     sc.register_license("CC-BY-SA-4.0")
-    assert sc.content == ""
+    assert str(sc) == ""
     sc.add_licence_and_copyrights()
-    assert ".. SPDX-License-Identifier: CC-BY-SA-4.0\n\n" == sc.content
+    assert str(sc) == """.. SPDX-License-Identifier: CC-BY-SA-4.0
+
+"""
 
 
@@ -108,25 +114,11 @@
         sc.register_license("x")
     sc.register_copyright("Copyright (C) A")
-    assert sc.content == ""
+    assert str(sc) == ""
     sc.add_licence_and_copyrights()
-    assert sc.content == """.. SPDX-License-Identifier: CC-BY-SA-4.0
+    assert str(sc) == """.. SPDX-License-Identifier: CC-BY-SA-4.0
 
 .. Copyright (C) A
 
 """
-
-
-def test_write(tmpdir):
-    sc = SphinxContent()
-    sc.add_line("x")
-    path = os.path.join(tmpdir, "x", "y")
-    sc.write(path)
-    with open(path, "r") as src:
-        assert "x\n" == src.read()
-    tmpdir.chdir()
-    path = "z"
-    sc.write(path)
-    with open(path, "r") as src:
-        assert "x\n" == src.read()
 
 
Index: rtemsqual/validation.py
===================================================================
--- rtemsqual/validation.py	(revision 22ec40f5ec8ba636f9dc5442c7025d3ffb0aba68)
+++ rtemsqual/validation.py	(revision 520ba1dd6db7eee5963128c02ab0ae51e77f610a)
@@ -66,25 +66,15 @@
 def _add_ingroup(content: CContent, items: List[Item], prefix: str,
                  key: str) -> None:
-    for desi in sorted(set(_designator(item[key]) for item in items)):
-        content.add(f""" * @ingroup {prefix}{desi}
-""")
+    content.add_ingroup(
+        [f"{prefix}{_designator(item[key])}" for item in items])
 
 
 def _add_test_case_description(content: CContent, item: Item,
                                test_case_to_suites: Dict[str, List[Item]],
-                               name: str, desi: str) -> None:
-    content.add(f"""
-/**
- * @defgroup RTEMSTestCase{desi} {name}
- *
-""")
-    _add_ingroup(content, test_case_to_suites[item.uid], "RTEMSTestSuite",
-                 "test-suite-name")
-    content.add(f""" *
- * @brief Test Case
- *
- * @{{
- */
-""")
+                               identifier: str, name: str) -> None:
+    with content.defgroup_block(f"RTEMSTestCase{identifier}", name):
+        _add_ingroup(content, test_case_to_suites[item.uid], "RTEMSTestSuite",
+                     "test-suite-name")
+        content.add(["@brief Test Case", "", "@{"])
 
 
@@ -92,23 +82,17 @@
     actions = item["test-case-actions"]
     if actions:
-        content.add_comment_content(
-            "This test case performs the following actions:")
+        content.add("This test case performs the following actions:")
         for action in actions:
-            content.add_comment_content(action["description"], intro="- ")
+            content.add(content.wrap(action["description"], intro="- "))
             for check in action["checks"]:
-                content.add_comment_content(check["description"], intro="  - ")
+                content.add(content.wrap(check["description"], intro="  - "))
 
 
 def _generate_test_case_actions(item: Item, steps: StepWrapper) -> CContent:
     content = CContent()
-    first = True
     for action in item["test-case-actions"]:
-        if not first:
-            content.add_blank_line()
-        first = False
-        content.add_lines(action["action"], indent=1)
+        content.add(action["action"])
         for check in action["checks"]:
-            the_check = string.Template(check["check"]).substitute(steps)
-            content.add_lines(the_check, indent=1)
+            content.append(string.Template(check["check"]).substitute(steps))
     return content
 
@@ -118,67 +102,36 @@
     name = item["test-case-name"]
     desi = _designator(name)
-    _add_test_case_description(content, item, test_case_to_suites, name, desi)
-    content.add_line_block(item["test-case-support"])
-    content.add(f"""
-/**
- * @fn void T_case_body_{desi}(void)
-""")
-    content.add_brief_description(item["test-case-brief"])
-    content.add_comment_content(item["test-case-description"])
-    _add_test_case_action_description(content, item)
-    content.add(f""" */
-""")
+    _add_test_case_description(content, item, test_case_to_suites, desi, name)
+    content.add(item["test-case-support"])
+    with content.function_block(f"void T_case_body_{desi}(void)"):
+        content.add_brief_description(item["test-case-brief"])
+        content.add(content.wrap(item["test-case-description"]))
+        _add_test_case_action_description(content, item)
     fixture = item["test-case-fixture"]
     if fixture:
-        content.add_lines(f"T_TEST_CASE_FIXTURE({desi}, &{fixture})")
+        content.append(f"T_TEST_CASE_FIXTURE({desi}, &{fixture})")
     else:
-        content.add_lines(f"T_TEST_CASE({desi})")
-    content.add_lines("{")
-    prologue = item["test-case-prologue"]
-    first = True
-    if prologue:
-        first = False
-        content.add_lines(prologue, indent=1)
-    steps = StepWrapper()
-    action_content = _generate_test_case_actions(item, steps)
-    if steps.steps > 0:
-        if not first:
-            content.add_blank_line()
-        first = False
-        content.add_lines(f"T_plan({steps.steps});", indent=1)
-    if action_content.content:
-        if not first:
-            content.add_blank_line()
-        first = False
-        content.add(action_content.content)
-    epilogue = item["test-case-epilogue"]
-    if epilogue:
-        if not first:
-            content.add_blank_line()
-        content.add_lines(epilogue, indent=1)
-    content.add(f"""}}
-
-/** @}} */
-""")
+        content.append(f"T_TEST_CASE({desi})")
+    content.append("{")
+    content.gap = False
+    with content.indent():
+        content.add(item["test-case-prologue"])
+        steps = StepWrapper()
+        action_content = _generate_test_case_actions(item, steps)
+        if steps.steps > 0:
+            content.add(f"T_plan({steps.steps});")
+        content.add(action_content)
+        content.add(item["test-case-epilogue"])
+    content.append(["}", "", "/** @} */"])
 
 
 def _generate_test_suite(content: CContent, item: Item) -> None:
     name = item["test-suite-name"]
-    content.add(f"""
-/**
- * @defgroup RTEMSTestSuite{_designator(name)} {name}
- *
- * @ingroup RTEMSTestSuites
- *
- * @brief Test Suite
-""")
-    content.add_comment_content(item["test-suite-description"])
-    content.add(f""" *
- * @{{
- */
-
-{item["test-suite-code"]}
-/** @}} */
-""")
+    with content.defgroup_block(f"RTEMSTestSuite{_designator(name)}", name):
+        content.add(["@ingroup RTEMSTestSuites", "", "@brief Test Suite"])
+        content.add(content.wrap(item["test-suite-description"]))
+        content.add("@{")
+    content.add(item["test-suite-code"])
+    content.add("/** @} */")
 
 
@@ -222,15 +175,9 @@
             item.register_license_and_copyrights(content)
         content.add_spdx_license_identifier()
-        content.add(f"""
-/**
- * @file
- *
-""")
-        _add_ingroup(content, self._test_suites, "RTEMSTestSuite",
-                     "test-suite-name")
-        _add_ingroup(content, self._test_cases, "RTEMSTestCase",
-                     "test-case-name")
-        content.add(f""" */
-""")
+        with content.file_block():
+            _add_ingroup(content, self._test_suites, "RTEMSTestSuite",
+                         "test-suite-name")
+            _add_ingroup(content, self._test_cases, "RTEMSTestCase",
+                         "test-case-name")
         content.add_copyrights_and_licenses()
         content.add_have_config()
