Repository: common-workflow-language/schema_salad (language: python)

Issue:
Update mistune requirement from <0.9,>=0.8.1 to >=0.8.1,<2.1
Updates the requirements on [mistune](https://github.com/lepture/mistune) to permit the latest version.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a href="https://github.com/lepture/mistune/releases">mistune's releases</a>.</em></p>
<blockquote>
<h2>Version 2.0.0</h2>
<p>First release of Mistune v2.</p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a href="https://github.com/lepture/mistune/blob/master/docs/changes.rst">mistune's changelog</a>.</em></p>
<blockquote>
<h2>Changelog</h2>
<p>Here is the full history of mistune v2.</p>
<p>Version 2.0.0</p>
<pre><code>
Released on Dec 5, 2021
<p>This is the first non-alpha release of mistune v2.</p>
<p>Version 2.0.0rc1
</code></pre></p>
<p>Released on Feb 16, 2021</p>
<p>Version 2.0.0a6</p>
<pre><code>
Released on Nov 26, 2020
<p>Version 2.0.0a5
</code></pre></p>
<p>Released on Sep 2, 2020</p>
<p>Version 2.0.0a4</p>
<pre><code>
Released on Apr 19, 2020
<p>Version 2.0.0a3
</code></pre></p>
<p>Released on Apr 11, 2020</p>
<p>Version 2.0.0a2</p>
<pre><code>
Released on Jan 1, 2020
<p>Version 2.0.0a1
</code></pre></p>
<p>Released on Dec 7, 2019</p>
<p>This is the first release of v2. Features included:</p>
<ul>
<li>whole new mistune</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/lepture/mistune/commit/e8fc67cc73bc8afcb0a620d2d4b762efff182516"><code>e8fc67c</code></a> Version bump 2.0.0</li>
<li><a href="https://github.com/lepture/mistune/commit/056a10e115cef9def33b8fb583151b6eb8c9eecc"><code>056a10e</code></a> Documentation to write a directive (<a href="https://github-redirect.dependabot.com/lepture/mistune/issues/289">#289</a>)</li>
<li><a href="https://github.com/lepture/mistune/commit/9b2bbec5d6ab98391bf5a786ee93bfec773e0119"><code>9b2bbec</code></a> fix setup.cfg url</li>
<li><a href="https://github.com/lepture/mistune/commit/0d13037e2c87528af4215e9490c6ae16daec0bae"><code>0d13037</code></a> Merge pull request <a href="https://github-redirect.dependabot.com/lepture/mistune/issues/286">#286</a> from hugovk/add-3.10</li>
<li><a href="https://github.com/lepture/mistune/commit/ea7678e1f28ff0710d9a1a0b3ec92a56c04d41c4"><code>ea7678e</code></a> Add support for Python 3.10</li>
<li><a href="https://github.com/lepture/mistune/commit/7d659b6fdbc25319701964146bdc15629ffaf406"><code>7d659b6</code></a> use python -m build</li>
<li><a href="https://github.com/lepture/mistune/commit/9fa6f3ac4022da4677c662f7524a952f5cd6392b"><code>9fa6f3a</code></a> fix: parse block quote text</li>
<li><a href="https://github.com/lepture/mistune/commit/81c75c9bea720e19f9bc67d3d2debd5c3708b823"><code>81c75c9</code></a> Make admonition directive html more similar to python-markdown (<a href="https://github-redirect.dependabot.com/lepture/mistune/issues/283">#283</a>)</li>
<li><a href="https://github.com/lepture/mistune/commit/e5f687cb95cd2a4b079c639a453b71442c268aa9"><code>e5f687c</code></a> Add hard_wrap option into create_markdown</li>
<li><a href="https://github.com/lepture/mistune/commit/a24729252a87514f8124c27ab93c3fea8b9bf7e6"><code>a247292</code></a> Prevent appending mailto: to http and https url.</li>
<li>Additional commits viewable in <a href="https://github.com/lepture/mistune/compare/v0.8.1...v2.0.0">compare view</a></li>
</ul>
</details>
<br />


Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)


</details>


Discussion / hints:
We need to decide if we want to upgrade to the new version only, or support pre 2.0 mistune
See https://github.com/lepture/mistune/issues/290
@dependabot ignore this major version
OK, I won't notify you about version 2.x.x again, unless you re-open this PR or update to a 2.x.x release yourself.

Fix patch:
diff --git a/.dockerignore b/.dockerignore
index 903cf938..b0e9524f 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -1,5 +1,6 @@
 .coverage
 coverage.xml
+pydocstyle_report.txt
 .tox/
 .eggs/
 .vscode/
diff --git a/.gitignore b/.gitignore
index 6b05935c..e135b8bc 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,14 +2,19 @@
 .eggs
 *.egg-info/
 *pyc
+*.so
 
 build/
 dist/
 mypy-stubs/ruamel/yaml
-# virtualenv 
+# virtualenv
 venv/
 .cache/
+.mypy_cache/
 .pytest_cache/
+.coverage
+coverage.xml
+pydocstyle_report.txt
 
 # PyCharm
 .idea/
diff --git a/Makefile b/Makefile
index 2488c927..86d990b2 100644
--- a/Makefile
+++ b/Makefile
@@ -185,6 +185,23 @@ mypy_3.6: $(filter-out setup.py,$(PYSOURCES))
 mypyc: $(PYSOURCES)
 	MYPYPATH=mypy-stubs SCHEMA_SALAD_USE_MYPYC=1 python setup.py test
 
+mypyi:
+	MYPYPATH=mypy-stubs SCHEMA_SALAD_USE_MYPYC=1 python setup.py install
+
+check-metaschema-diff:
+	docker run \
+		-v "$(realpath ${MODULE}/metaschema/):/tmp/:ro" \
+		"quay.io/commonwl/cwltool_module:latest" \
+		schema-salad-doc /tmp/metaschema.yml \
+		> /tmp/metaschema.orig.html
+	schema-salad-doc \
+		"$(realpath ${MODULE}/metaschema/metaschema.yml)" \
+		> /tmp/metaschema.new.html
+	diff -a --color /tmp/metaschema.orig.html /tmp/metaschema.new.html || true
+
+compute-metaschema-hash:
+	@python -c 'import hashlib; from schema_salad.tests.test_makedoc import generate_doc; hasher = hashlib.sha256(); hasher.update(generate_doc().encode("utf-8")); print(hasher.hexdigest());'
+
 shellcheck: FORCE
 	shellcheck build-schema_salad-docker.sh release-test.sh
 
diff --git a/mypy-stubs/mistune.pyi b/mypy-stubs/mistune.pyi
deleted file mode 100644
index b4fe1021..00000000
--- a/mypy-stubs/mistune.pyi
+++ /dev/null
@@ -1,197 +0,0 @@
-__author__ = "Aleksandr Slepchenkov"
-__email__ = "Sl.aleksandr28@gmail.com"
-
-from typing import (
-    Any,
-    Dict,
-    Iterable,
-    List,
-    Match,
-    Optional,
-    Pattern,
-    Sequence,
-    Tuple,
-    Type,
-)
-
-Tokens = List[Dict[str, Any]]
-# There are too much levels of optional unions of lists of text in cell and align 385 and 396 lines in mistune
-
-def escape(text: str, quote: bool = ..., smart_amp: bool = ...) -> str: ...
-
-class BlockGrammar:
-    def_links: Pattern[str]
-    def_footnotes: Pattern[str]
-    newline: Pattern[str]
-    block_code: Pattern[str]
-    fences: Pattern[str]
-    hrule: Pattern[str]
-    heading: Pattern[str]
-    lheading: Pattern[str]
-    block_quote: Pattern[str]
-    list_block: Pattern[str]
-    list_item: Pattern[str]
-    list_bullet: Pattern[str]
-    paragraph: Pattern[str]
-    block_html: Pattern[str]
-    table: Pattern[str]
-    nptable: Pattern[str]
-    text: Pattern[str]
-
-class BlockLexer:
-    grammar_class: Type[BlockGrammar]
-    default_rules: List[str]
-    list_rules: Tuple[str]
-    footnote_rules: Tuple[str]
-    tokens: Tokens
-    def_links: Dict[str, Dict[str, str]]
-    def_footnotes: Dict[str, int]
-    rules = ...  # type: BlockGrammar
-    def __init__(self, rules: Optional[BlockGrammar] = ..., **kwargs: Any) -> None: ...
-    def __call__(self, text: str, rules: Optional[Sequence[str]] = ...) -> Tokens: ...
-    def parse(self, text: str, rules: Optional[Sequence[str]] = ...) -> Tokens: ...
-    def parse_newline(self, m: Match[str]) -> None: ...
-    def parse_block_code(self, m: Match[str]) -> None: ...
-    def parse_fences(self, m: Match[str]) -> None: ...
-    def parse_heading(self, m: Match[str]) -> None: ...
-    def parse_lheading(self, m: Match[str]) -> None: ...
-    def parse_hrule(self, m: Match[str]) -> None: ...
-    def parse_list_block(self, m: Match[str]) -> None: ...
-    def parse_block_quote(self, m: Match[str]) -> None: ...
-    def parse_def_links(self, m: Match[str]) -> None: ...
-    def parse_def_footnotes(self, m: Match[str]) -> None: ...
-    def parse_table(self, m: Match[str]) -> None: ...
-    def parse_nptable(self, m: Match[str]) -> None: ...
-    def parse_block_html(self, m: Match[str]) -> None: ...
-    def parse_paragraph(self, m: Match[str]) -> None: ...
-    def parse_text(self, m: Match[str]) -> None: ...
-
-class InlineGrammar:
-    escape: Pattern[str]
-    inline_html: Pattern[str]
-    autolink: Pattern[str]
-    link: Pattern[str]
-    reflink: Pattern[str]
-    nolink: Pattern[str]
-    url: Pattern[str]
-    double_emphasis: Pattern[str]
-    emphasis: Pattern[str]
-    code: Pattern[str]
-    linebreak: Pattern[str]
-    strikethrough: Pattern[str]
-    footnote: Pattern[str]
-    text: Pattern[str]
-    def hard_wrap(self) -> None: ...
-
-class InlineLexer:
-    grammar_class: Type[InlineGrammar]
-    default_rules: List[str]
-    inline_html_rules: List[str]
-    renderer: Renderer
-    links: Dict[str, Dict[str, str]]
-    footnotes: Dict[str, int]
-    footnote_index: int
-    _in_link: bool
-    _in_footnote: bool
-    _parse_inline_html: bool
-    rules: InlineGrammar
-    def __init__(
-        self, renderer: Renderer, rules: Optional[InlineGrammar] = ..., **kwargs: Any
-    ) -> None: ...
-    def __call__(self, text: str, rules: Optional[Sequence[str]] = ...) -> str: ...
-    def setup(
-        self,
-        links: Optional[Dict[str, Dict[str, str]]],
-        footnotes: Optional[Dict[str, int]],
-    ) -> None: ...
-    line_match: Match[str]
-    line_started: bool
-    def output(self, text: str, rules: Optional[Sequence[str]] = ...) -> str: ...
-    def output_escape(self, m: Match[str]) -> str: ...
-    def output_autolink(self, m: Match[str]) -> str: ...
-    def output_url(self, m: Match[str]) -> str: ...
-    def output_inline_html(self, m: Match[str]) -> str: ...
-    def output_footnote(self, m: Match[str]) -> Optional[str]: ...
-    def output_link(self, m: Match[str]) -> str: ...
-    def output_reflink(self, m: Match[str]) -> Optional[str]: ...
-    def output_nolink(self, m: Match[str]) -> Optional[str]: ...
-    def output_double_emphasis(self, m: Match[str]) -> str: ...
-    def output_emphasis(self, m: Match[str]) -> str: ...
-    def output_code(self, m: Match[str]) -> str: ...
-    def output_linebreak(self, m: Match[str]) -> str: ...
-    def output_strikethrough(self, m: Match[str]) -> str: ...
-    def output_text(self, m: Match[str]) -> str: ...
-
-class Renderer:
-    options: Dict[str, str]
-    def __init__(self, **kwargs: Any) -> None: ...
-    def placeholder(self) -> str: ...
-    def block_code(
-        self, code: str, lang: Any = ...
-    ) -> str: ...  # It seems that lang should be string, however other types are valid as well
-    def block_quote(self, text: str) -> str: ...
-    def block_html(self, html: str) -> str: ...
-    def header(self, text: str, level: int, raw: Optional[str] = ...) -> str: ...
-    def hrule(self) -> str: ...
-    def list(
-        self, body: Any, ordered: bool = ...
-    ) -> str: ...  # body - same reason as for lang above, and for other Any in this class
-    def list_item(self, text: Any) -> str: ...
-    def paragraph(self, text: str) -> str: ...
-    def table(self, header: Any, body: Any) -> str: ...
-    def table_row(self, content: Any) -> str: ...
-    def table_cell(self, content: Any, **flags: Dict[str, Any]) -> str: ...
-    def double_emphasis(self, text: Any) -> str: ...
-    def emphasis(self, text: Any) -> str: ...
-    def codespan(self, text: str) -> str: ...
-    def linebreak(self) -> str: ...
-    def strikethrough(self, text: Any) -> str: ...
-    def text(self, text: Any) -> str: ...
-    def escape(self, text: Any) -> str: ...
-    def autolink(self, link: Any, is_email: bool = ...) -> str: ...
-    def link(self, link: Any, title: Any, text: Any) -> str: ...
-    def image(self, src: Any, title: Any, text: Any) -> str: ...
-    def inline_html(self, html: Any) -> str: ...
-    def newline(self) -> str: ...
-    def footnote_ref(self, key: Any, index: int) -> str: ...
-    def footnote_item(self, key: Any, text: str) -> str: ...
-    def footnotes(self, text: Any) -> str: ...
-
-class Markdown:
-    renderer = ...  # type: Renderer
-    inline = ...  # type: InlineLexer
-    block = ...  # type: BlockLexer
-    footnotes = ...  # type: List[Dict[str, Any]]
-    tokens = ...  # type: Tokens
-    def __init__(
-        self,
-        renderer: Optional[Renderer] = ...,
-        inline: Optional[InlineLexer] = ...,
-        block: Optional[BlockLexer] = ...,
-        **kwargs: Any
-    ) -> None: ...
-    def __call__(self, text: str) -> str: ...
-    def render(self, text: str) -> str: ...
-    def parse(self, text: str) -> str: ...
-    token = ...  # type: Dict[str, Any]
-    def pop(self) -> Optional[Dict[str, Any]]: ...
-    def peek(self) -> Optional[Dict[str, Any]]: ...
-    def output(self, text: str, rules: Optional[Sequence[str]] = ...) -> str: ...
-    def tok(self) -> str: ...
-    def tok_text(self) -> str: ...
-    def output_newline(self) -> str: ...
-    def output_hrule(self) -> str: ...
-    def output_heading(self) -> str: ...
-    def output_code(self) -> str: ...
-    def output_table(self) -> str: ...
-    def output_block_quote(self) -> str: ...
-    def output_list(self) -> str: ...
-    def output_list_item(self) -> str: ...
-    def output_loose_item(self) -> str: ...
-    def output_footnote(self) -> str: ...
-    def output_close_html(self) -> str: ...
-    def output_open_html(self) -> str: ...
-    def output_paragraph(self) -> str: ...
-    def output_text(self) -> str: ...
-
-def markdown(text: str, escape: bool = ..., **kwargs: Any) -> str: ...
diff --git a/mypy-stubs/mistune/__init__.pyi b/mypy-stubs/mistune/__init__.pyi
new file mode 100644
index 00000000..6938dbeb
--- /dev/null
+++ b/mypy-stubs/mistune/__init__.pyi
@@ -0,0 +1,28 @@
+from typing import Iterable, Optional, Union
+
+from mistune._types import *
+from mistune.inline_parser import RendererT
+from mistune.markdown import Markdown, ParseHook, RenderHook
+from mistune.plugins import Plugin, PluginName
+from mistune.renderers import BaseRenderer, DataT, HTMLRenderer, HTMLType
+from typing_extensions import Literal
+
+html: Markdown[HTMLType, HTMLRenderer]
+
+RendererRef = Union[Literal["html", "ast"], BaseRenderer[DataT]]
+PluginRef = Union[PluginName, Plugin]  # reference to register a plugin
+
+def create_markdown(
+    escape: bool = False,
+    hard_wrap: bool = False,
+    renderer: Optional[RendererRef[DataT]] = None,
+    plugins: Optional[Iterable[PluginRef]] = None,
+) -> Markdown[DataT, RendererT]: ...
+def markdown(
+    text: str,
+    escape: bool = True,
+    renderer: Optional[BaseRenderer[DataT]] = None,
+    plugins: Optional[Iterable[PluginRef]] = None,
+) -> str: ...
+
+__version__: str
diff --git a/mypy-stubs/mistune/_types.pyi b/mypy-stubs/mistune/_types.pyi
new file mode 100644
index 00000000..0ebbd5c1
--- /dev/null
+++ b/mypy-stubs/mistune/_types.pyi
@@ -0,0 +1,5 @@
+from typing import Any, Dict, List
+
+# type aliases shared across modules
+State = Dict[str, Any]  # extra options that work with a given 'ParsedType'
+Tokens = List[str]
diff --git a/mypy-stubs/mistune/block_parser.pyi b/mypy-stubs/mistune/block_parser.pyi
new file mode 100644
index 00000000..a7b482de
--- /dev/null
+++ b/mypy-stubs/mistune/block_parser.pyi
@@ -0,0 +1,161 @@
+import sys
+from typing import (
+    Any,
+    ClassVar,
+    Iterator,
+    List,
+    Match,
+    Optional,
+    Pattern,
+    Tuple,
+    Type,
+    TypeVar,
+    Union,
+)
+
+from mistune._types import State, Tokens
+from mistune.inline_parser import InlineParser, RendererT
+from mistune.renderers import DataT
+from mistune.scanner import Matcher, ScannerParser
+from typing_extensions import Literal, NotRequired, Required, TypeAlias, TypedDict
+
+ParsedBlockType = Literal[
+    # base block parsers
+    "heading",
+    "newline",
+    "thematic_break",
+    "block_code",
+    "block_html",
+    "block_quote",
+    "block_text",
+    "list",
+    "list_item",
+    "paragraph",
+    # plugin 'table'
+    "table",
+    "table_head",
+    "table_body",
+    "table_cell",
+    "table_row",
+    # plugin 'url'
+    "url",
+]
+_ParsedBlock: TypeAlias = "ParsedBlock"
+if sys.version_info >= (3, 7):
+    ParsedTypeT = TypeVar("ParsedTypeT")
+    ParsedBlock = TypedDict(
+        "ParsedBlock",
+        {
+            "type": Required[ParsedTypeT],
+            "blank": NotRequired[bool],
+            "raw": NotRequired[str],
+            "text": NotRequired[str],
+            "params": NotRequired[Tuple[Union[int, str], ...]],
+            "children": NotRequired[List[_ParsedBlock]],
+        },
+        total=False,
+    )
+    ParsedBlockHeading = ParsedBlock[Literal["heading"]]
+    ParsedBlockNewline = ParsedBlock[Literal["newline"]]
+    ParsedBlockThematicBreak = ParsedBlock[Literal["thematic_break"]]
+    ParsedBlockBlockCode = ParsedBlock[Literal["block_code"]]
+    ParsedBlockBlockHTML = ParsedBlock[Literal["block_html"]]
+    ParsedBlockBlockQuote = ParsedBlock[Literal["block_quote"]]
+    ParsedBlockBlockText = ParsedBlock[Literal["block_text"]]
+    ParsedBlockList = ParsedBlock[Literal["list"]]
+    ParsedBlockListItem = ParsedBlock[Literal["list_item"]]
+    ParsedBlockParagraph = ParsedBlock[Literal["paragraph"]]
+else:  # python 3.6  # no TypedDict+Generic support with TypeVar
+    ParsedBlock = TypedDict(
+        "ParsedBlock",
+        {
+            # best we can do is define an 'AnyOf' allowed literals
+            # we cannot provide explicitly which literal is returned each time
+            "type": Required[ParsedBlockType],
+            "blank": NotRequired[bool],
+            "raw": NotRequired[str],
+            "text": NotRequired[str],
+            "params": NotRequired[Tuple[Union[int, str], ...]],
+            "children": NotRequired[List[_ParsedBlock]],
+        },
+        total=False,
+    )
+    ParsedBlockHeading = ParsedBlock
+    ParsedBlockNewline = ParsedBlock
+    ParsedBlockThematicBreak = ParsedBlock
+    ParsedBlockBlockCode = ParsedBlock
+    ParsedBlockBlockHTML = ParsedBlock
+    ParsedBlockBlockQuote = ParsedBlock
+    ParsedBlockBlockText = ParsedBlock
+    ParsedBlockList = ParsedBlock
+    ParsedBlockListItem = ParsedBlock
+    ParsedBlockParagraph = ParsedBlock
+
+class BlockParser(ScannerParser):
+    NEWLINE: Pattern[str]
+    DEF_LINK: Pattern[str]
+    AXT_HEADING: Pattern[str]
+    SETEX_HEADING: Pattern[str]
+    THEMATIC_BREAK: Pattern[str]
+    INDENT_CODE: Pattern[str]
+    FENCED_CODE: Pattern[str]
+    BLOCK_QUOTE: Pattern[str]
+    LIST_START: Pattern[str]
+    BLOCK_HTML: Pattern[str]
+    LIST_MAX_DEPTH: ClassVar[int]
+    BLOCK_QUOTE_MAX_DEPTH: ClassVar[int]
+
+    scanner_cls: ClassVar[Type[Matcher]] = Matcher
+    block_quote_rules: List[str]
+    list_rules = List[str]
+
+    def __init__(self) -> None: ...
+    def parse_newline(self, m: Match[str], state: State) -> ParsedBlockNewline: ...
+    def parse_thematic_break(
+        self, m: Match[str], state: State
+    ) -> ParsedBlockThematicBreak: ...
+    def parse_indent_code(
+        self, m: Match[str], state: State
+    ) -> ParsedBlockBlockCode: ...
+    def parse_fenced_code(
+        self, m: Match[str], state: State
+    ) -> ParsedBlockBlockCode: ...
+    def tokenize_block_code(
+        self, code: str, info: Tuple[str, ...], state: State
+    ) -> ParsedBlockBlockCode: ...
+    def parse_axt_heading(self, m: Match[str], state: State) -> ParsedBlockHeading: ...
+    def parse_setex_heading(
+        self, m: Match[str], state: State
+    ) -> ParsedBlockHeading: ...
+    def tokenize_heading(
+        self, text: str, level: int, state: State
+    ) -> ParsedBlockHeading: ...
+    def get_block_quote_rules(self, depth: int) -> List[str]: ...
+    def parse_block_quote(
+        self, m: Match[str], state: State
+    ) -> ParsedBlockBlockQuote: ...
+    def get_list_rules(self, depth: int) -> List[str]: ...
+    def parse_list_start(
+        self, m: Match[str], state: State, string: str
+    ) -> Tuple[ParsedBlockList, int]: ...
+    def parse_list_item(
+        self, text: str, depth: int, state: State, rules: List[str]
+    ) -> ParsedBlockListItem: ...
+    def normalize_list_item_text(self, text: str) -> str: ...
+    def parse_block_html(self, m: Match[str], state: State) -> ParsedBlockBlockHTML: ...
+    def parse_def_link(self, m: Match[str], state: State) -> None: ...
+    def parse_text(
+        self, text: str, state: State
+    ) -> Union[ParsedBlockBlockText, List[ParsedBlockParagraph]]: ...
+    def parse(
+        self, s: str, state: State, rules: Optional[List[str]] = None
+    ) -> Tokens: ...
+    def render(
+        self, tokens: Tokens, inline: InlineParser[RendererT], state: State
+    ) -> Any: ...  # technically DataT, but defined by 'inline.renderer.finalize'
+    def _iter_render(
+        self, tokens: Tokens, inline: InlineParser[RendererT], state: State
+    ) -> Iterator[DataT]: ...
+
+def cleanup_lines(s: str) -> str: ...
+def expand_leading_tab(text: str) -> str: ...
diff --git a/mypy-stubs/mistune/inline_parser.pyi b/mypy-stubs/mistune/inline_parser.pyi
new file mode 100644
index 00000000..40836f17
--- /dev/null
+++ b/mypy-stubs/mistune/inline_parser.pyi
@@ -0,0 +1,86 @@
+from typing import (
+    ClassVar,
+    Generic,
+    Iterator,
+    List,
+    Match,
+    Optional,
+    Pattern,
+    Tuple,
+    TypeVar,
+    Union,
+)
+
+from mistune._types import State
+from mistune.renderers import AstRenderer, DataT, HTMLRenderer
+from mistune.scanner import ScannerParser
+from mistune.util import ESCAPE_TEXT
+from typing_extensions import Literal
+
+HTML_TAGNAME: str
+HTML_ATTRIBUTES: str
+ESCAPE_CHAR: Pattern[str]
+LINK_TEXT: str
+LINK_LABEL: str
+
+# Union[BaseRenderer[DataT], AstRenderer, HTMLRenderer]
+RendererT = TypeVar("RendererT", bound=Union[AstRenderer, HTMLRenderer])
+
+class InlineParser(ScannerParser, Generic[RendererT]):
+    ESCAPE: ClassVar[str] = ESCAPE_TEXT
+    AUTO_LINK: ClassVar[str]
+    STD_LINK: ClassVar[str]
+    REF_LINK: ClassVar[str]
+    REF_LINK2: ClassVar[str]
+    ASTERISK_EMPHASIS: ClassVar[str]
+    UNDERSCORE_EMPHASIS: ClassVar[str]
+    CODESPAN: ClassVar[str]
+    LINEBREAK: ClassVar[str]
+    INLINE_HTML: ClassVar[str]
+    RULE_NAMES: ClassVar[Tuple[str, ...]]
+
+    renderer: RendererT
+
+    def __init__(self, renderer: RendererT, hard_wrap: bool = False) -> None: ...
+    def parse_escape(
+        self, m: Match[str], state: State
+    ) -> Tuple[Literal["text"], str]: ...
+    def parse_autolink(
+        self, m: Match[str], state: State
+    ) -> Tuple[Literal["link"], str, str]: ...
+    def parse_std_link(
+        self, m: Match[str], state: State
+    ) -> Tuple[Literal["link"], str, str, str]: ...
+    def parse_ref_link(
+        self, m: Match[str], state: State
+    ) -> Tuple[Literal["link"], str, str, str]: ...
+    def parse_ref_link2(
+        self, m: Match[str], state: State
+    ) -> Tuple[Literal["link"], str, str, str]: ...
+    def tokenize_link(
+        self, line: str, link: str, text: str, title: str, state: State
+    ) -> Tuple[Literal["link"], str, str, str]: ...
+    def parse_asterisk_emphasis(
+        self, m: Match[str], state: State
+    ) -> Tuple[Literal["strong", "emphasis"], str]: ...
+    def parse_underscore_emphasis(
+        self, m: Match[str], state: State
+    ) -> Tuple[Literal["strong", "emphasis"], str]: ...
+    def tokenize_emphasis(self, m: Match[str], state: State) -> Tuple[str, str]: ...
+    def parse_codespan(
+        self, m: Match[str], state: State
+    ) -> Tuple[Literal["codespan"], str]: ...
+    def parse_linebreak(
+        self, m: Match[str], state: State
+    ) -> Tuple[Literal["linebreak"], str]: ...
+    def parse_inline_html(
+        self, m: Match[str], state: State
+    ) -> Tuple[Literal["inline_html"], str]: ...
+    def parse_text(self, text: str, state: State) -> Tuple[Literal["text"], str]: ...
+    def parse(
+        self, s: str, state: State, rules: Optional[List[str]]
+    ) -> Iterator[DataT]: ...
+    def render(
+        self, s: str, state: State, rules: Optional[List[str]]
+    ) -> Union[DataT, List[DataT]]: ...
+    def __call__(self, s: str, state: State) -> Union[DataT, List[DataT]]: ...
diff --git a/mypy-stubs/mistune/markdown.pyi b/mypy-stubs/mistune/markdown.pyi
new file mode 100644
index 00000000..7e744b09
--- /dev/null
+++ b/mypy-stubs/mistune/markdown.pyi
@@ -0,0 +1,35 @@
+from typing import Any, Callable, Dict, Generic, Iterable, List, Match, Optional, Tuple
+
+from mistune._types import State
+from mistune.block_parser import BlockParser
+from mistune.inline_parser import InlineParser, RendererT
+from mistune.plugins import Plugin
+from mistune.renderers import BaseRenderer, DataT
+
+Tokens = List[Dict[str, Any]]
+ParseHook = Callable[[Markdown[DataT, RendererT], DataT, State], Tuple[str, State]]
+RenderHook = Callable[[Markdown[DataT, RendererT], Tokens, State], Tokens]
+
+class Markdown(Generic[DataT, RendererT]):
+    renderer: BaseRenderer[DataT]
+    inline: InlineParser[RendererT]
+    block: BlockParser
+    before_parse_hooks: List[ParseHook[DataT, RendererT]]
+    before_render_hooks: List[RenderHook[DataT, RendererT]]
+    after_render_hooks: List[RenderHook[DataT, RendererT]]
+
+    def __init__(
+        self,
+        renderer: BaseRenderer[DataT],
+        block: Optional[BlockParser] = None,
+        inline: Optional[InlineParser[RendererT]] = None,
+        plugins: Optional[Iterable[Plugin]] = None,
+    ) -> None: ...
+    def before_parse(self, s: str, state: State) -> Tuple[str, State]: ...
+    def before_render(self, tokens: Tokens, state: State) -> Tokens: ...
+    def after_render(self, tokens: Tokens, state: State) -> Tokens: ...
+    def parse(self, s: str, state: Optional[State] = None) -> str: ...
+    def read(self, filepath: str, state: Optional[State] = None) -> str: ...
+    def __call__(self, s: str) -> str: ...
+
+def preprocess(s: str, state: State) -> Tuple[str, State]: ...
diff --git a/mypy-stubs/mistune/plugins/__init__.pyi b/mypy-stubs/mistune/plugins/__init__.pyi
new file mode 100644
index 00000000..f74d0543
--- /dev/null
+++ b/mypy-stubs/mistune/plugins/__init__.pyi
@@ -0,0 +1,21 @@
+from typing import Dict, Protocol
+
+from mistune.inline_parser import RendererT
+from mistune.markdown import Markdown
+from mistune.renderers import DataT
+from typing_extensions import Literal
+
+PluginName = Literal[
+    "url",
+    "strikethrough",
+    "footnotes",
+    "table",
+    "task_lists",
+    "def_list",
+    "abbr",
+]
+
+class Plugin(Protocol):
+    def __call__(self, markdown: Markdown[DataT, RendererT]) -> None: ...
+
+PLUGINS: Dict[PluginName, Plugin]
diff --git a/mypy-stubs/mistune/plugins/table.pyi b/mypy-stubs/mistune/plugins/table.pyi
new file mode 100644
index 00000000..8ea755c1
--- /dev/null
+++ b/mypy-stubs/mistune/plugins/table.pyi
@@ -0,0 +1,17 @@
+import sys
+from typing import Match
+
+from mistune._types import State
+from mistune.block_parser import BlockParser, ParsedBlock
+from mistune.inline_parser import RendererT
+from mistune.markdown import Markdown
+from mistune.renderers import DataT
+from typing_extensions import Literal
+
+if sys.version_info >= (3, 7):
+    ParsedBlockTable = ParsedBlock[Literal["paragraph"]]
+else:
+    ParsedBlockTable = ParsedBlock
+
+def parse_table(self: BlockParser, m: Match[str], state: State) -> ParsedBlockTable: ...
+def plugin_table(md: Markdown[DataT, RendererT]) -> None: ...
diff --git a/mypy-stubs/mistune/renderers.pyi b/mypy-stubs/mistune/renderers.pyi
new file mode 100644
index 00000000..8e317280
--- /dev/null
+++ b/mypy-stubs/mistune/renderers.pyi
@@ -0,0 +1,113 @@
+from typing import Any, Callable, ClassVar, Generic, List, Optional, Set, TypeVar, Union
+
+from typing_extensions import Literal, NotRequired, Required, TypeAlias, TypedDict
+
+AstType = Literal[
+    "text",
+    "image",
+    "codespan",
+    "linebreak",
+    "inline_html",
+    "heading",
+    "newline",
+    "thematic_break",
+    "block_code",
+    "block_html",
+    "list",
+    "list_item",
+]
+_AstChildren: TypeAlias = "AstChildren"
+AstObject = TypedDict(
+    "AstObject",
+    {
+        "type": Required[AstType],
+        "text": Required[Optional[_AstChildren]],
+        "link": NotRequired[str],
+        "info": NotRequired[Optional[str]],
+        "children": NotRequired[Union[_AstChildren, List[_AstChildren]]],
+        "ordered": NotRequired[bool],
+        "level": NotRequired[int],
+    },
+    total=False,
+)
+AstChildren = Union[AstObject, str]
+
+HTMLType = str
+DataT = TypeVar("DataT", bound=Union[AstChildren, HTMLType])
+
+RenderMethod = Union[
+    Callable[[], DataT],  # blank
+    Callable[[DataT, Any], DataT],
+]
+
+class BaseRenderer(Generic[DataT]):
+    NAME: ClassVar[str] = "base"
+
+    def _get_method(self, name: AstType) -> RenderMethod[DataT]: ...
+    def finalize(self, data: DataT) -> Any: ...
+
+class AstRenderer(BaseRenderer[AstChildren]):
+    NAME: ClassVar[Literal["ast"]] = "ast"
+
+    def __init__(
+        self, escape: bool = True, allow_harmful_protocols: Optional[bool] = None
+    ) -> None: ...
+    def _create_default_method(self, name: AstType) -> RenderMethod[AstChildren]: ...
+    def _get_method(self, name: AstType) -> RenderMethod[AstChildren]: ...
+    def text(self, text: str) -> AstChildren: ...
+    def link(
+        self, link: str, text: Optional[str] = None, title: Optional[str] = None
+    ) -> AstChildren: ...
+    def image(
+        self, src: str, alt: str = "", title: Optional[str] = None
+    ) -> AstChildren: ...
+    def codespan(self, text: str) -> AstChildren: ...
+    def linebreak(self, text: str) -> AstChildren: ...
+    def inline_html(self, text: str) -> AstChildren: ...
+    def paragraph(self, text: str) -> AstChildren: ...
+    def heading(self, text: str, level: int) -> AstChildren: ...
+    def newline(self) -> AstChildren: ...
+    def thematic_break(self) -> AstChildren: ...
+    def block_code(self, code: str, info: Optional[str] = None) -> AstChildren: ...
+    def block_html(self, html: str) -> AstChildren: ...
+    def list(
+        self, children: str, ordered: bool, level: int, start: Optional[int] = None
+    ) -> AstChildren: ...
+    def list_item(self, children: str, level: int) -> AstChildren: ...
+    def finalize(self, data: DataT) -> List[DataT]: ...
+
+class HTMLRenderer(BaseRenderer[HTMLType]):
+    NAME: ClassVar[Literal["html"]] = "html"
+    HARMFUL_PROTOCOLS: ClassVar[Set[str]]
+    _escape: bool
+    _allow_harmful_protocols: Optional[bool]
+
+    def __init__(
+        self, escape: bool = True, allow_harmful_protocols: Optional[bool] = None
+    ) -> None: ...
+    def _safe_url(self, url: str) -> str: ...
+    def text(self, text: str) -> HTMLType: ...
+    def link(
+        self, link: str, text: Optional[str] = None, title: Optional[str] = None
+    ) -> HTMLType: ...
+    def image(
+        self, src: str, alt: str = "", title: Optional[str] = None
+    ) -> HTMLType: ...
+    def emphasis(self, text: str) -> HTMLType: ...
+    def strong(self, text: str) -> HTMLType: ...
+    def codespan(self, text: str) -> HTMLType: ...
+    def linebreak(self, text: str) -> HTMLType: ...
+    def inline_html(self, text: str) -> HTMLType: ...
+    def paragraph(self, text: str) -> HTMLType: ...
+    def heading(self, text: str, level: int) -> HTMLType: ...
+    def newline(self) -> HTMLType: ...
+    def thematic_break(self) -> HTMLType: ...
+    def block_code(self, code: str, info: Optional[str] = None) -> HTMLType: ...
+    def block_quote(self, text: str) -> HTMLType: ...
+    def block_html(self, html: str) -> HTMLType: ...
+    def block_error(self, html: str) -> HTMLType: ...
+    def list(
+        self, text: str, ordered: bool, level: int, start: Optional[int] = None
+    ) -> HTMLType: ...
+    def list_item(self, text: str, level: int) -> HTMLType: ...
+    def finalize(self, data: DataT) -> HTMLType: ...
diff --git a/mypy-stubs/mistune/scanner.pyi b/mypy-stubs/mistune/scanner.pyi
new file mode 100644
index 00000000..b65b80e9
--- /dev/null
+++ b/mypy-stubs/mistune/scanner.pyi
@@ -0,0 +1,66 @@
+from typing import (
+    Any,
+    Callable,
+    ClassVar,
+    Dict,
+    Iterable,
+    Iterator,
+    List,
+    Match,
+    Pattern,
+    Tuple,
+    Type,
+    Union,
+)
+
+from mistune._types import State
+from typing_extensions import TypeAlias
+
+MethodFunc: TypeAlias = Callable[["ScannerParser", Match, State], Any]
+RuleMethod = Tuple[Pattern[str], MethodFunc]
+Lexicon = List[Tuple[Pattern[str], Tuple[str, RuleMethod]]]
+TextParser = Callable[[str, State], str]
+
+class SREScanner:  # real type unknown
+    def __init__(self, lexicon: Lexicon) -> None: ...
+    def scanner(self, string: str) -> Pattern[str]: ...
+
+class Scanner(object):
+    scanner: ClassVar[SREScanner]
+    lexicon: Lexicon
+
+    def iter(
+        self,
+        string: str,
+        state: State,
+        parse_text: TextParser,
+    ) -> Iterator[str]: ...
+
+class Matcher:
+    PARAGRAPH_END: ClassVar[Pattern[str]]
+    lexicon: Lexicon
+
+    def __init__(self, lexicon: Lexicon) -> None: ...
+    def search_pos(self, string: str, pos: int) -> int: ...
+    def iter(
+        self, string: str, state: State, parse_text: TextParser
+    ) -> Iterator[str]: ...
+
+class ScannerParser:
+    RULE_NAMES: ClassVar[Tuple[str, ...]]
+    scanner_cls: ClassVar[Union[Type[SREScanner], Type[Matcher]]]
+    rules: List[str]
+    rule_methods: Dict[str, RuleMethod]
+    _cached_sc: Dict[str, Scanner]
+
+    def __init__(self) -> None: ...
+    def register_rule(
+        self, name: str, patter: Pattern[str], method: RuleMethod
+    ) -> None: ...
+    def get_rule_pattern(self, name: str) -> Pattern[str]: ...
+    def get_rule_method(self, name: str) -> RuleMethod: ...
+    def parse_text(
+        self, text: str, state: State
+    ) -> Any: ...  # abstract method, return specialized by implementation
+    def _scan(self, s: str, state: State, rules: Iterable[str]) -> Iterator[str]: ...
+    def _create_scanner(self, rules: Iterable[str]) -> Scanner: ...
diff --git a/mypy-stubs/mistune/util.pyi b/mypy-stubs/mistune/util.pyi
new file mode 100644
index 00000000..27a1f06c
--- /dev/null
+++ b/mypy-stubs/mistune/util.pyi
@@ -0,0 +1,7 @@
+PUNCTUATION: str
+ESCAPE_TEXT: str
+
+def escape(text: str, quote: bool = True) -> str: ...
+def escape_html(s: str) -> str: ...
+def escape_url(s: str) -> str: ...
+def unikey(s: str) -> str: ...
diff --git a/mypy.ini b/mypy.ini
index 6a442f87..bb378a66 100644
--- a/mypy.ini
+++ b/mypy.ini
@@ -19,6 +19,7 @@ warn_unused_ignores = True
 warn_return_any = True
 implicit_reexport = False
 strict_equality = True
+enable_recursive_aliases = True
 
 [mypy-ruamel.*]
 ignore_errors = True
diff --git a/pyproject.toml b/pyproject.toml
index b93072fb..37c4ce90 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,5 +1,16 @@
 [build-system]
-requires = ["setuptools>=45", "setuptools_scm[toml]>=6.2", "mypy==0.990;python_version>'3.6'", "black>=19.10b0", "types-pkg_resources", "types-requests", "types-dataclasses", "ruamel.yaml>= 0.12.4, != 0.16.6, < 0.18", "types-setuptools"]
+requires = [
+    "setuptools>=45",
+    "setuptools_scm[toml]>=6.2",
+    'mypy==0.971; python_version == "3.6"',  # last version for Python 3.6
+    'mypy==0.991; python_version >= "3.7"',
+    "black>=19.10b0",
+    "types-pkg_resources",
+    "types-requests",
+    "types-dataclasses",
+    "ruamel.yaml>= 0.12.4, != 0.16.6, < 0.18",
+    "types-setuptools"
+]
 build-backend = "setuptools.build_meta"
 
 [tool.setuptools_scm]
diff --git a/requirements.txt b/requirements.txt
index 7b2b3b99..8adc7e8b 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -3,6 +3,6 @@ ruamel.yaml >= 0.16.12, < 0.17.22
 rdflib>= 4.2.2, < 7.0.0
 rdflib>= 4.2.2, < 6.0.0;python_version<='3.6'
 rdflib-jsonld>=0.4.0, <= 0.6.1;python_version<='3.6'
-mistune>=0.8.1,<0.9
+mistune>=2.0.3,<2.1
 CacheControl[filecache]==0.12.11
 black
diff --git a/schema_salad/makedoc.py b/schema_salad/makedoc.py
index 38a255fb..78d82515 100644
--- a/schema_salad/makedoc.py
+++ b/schema_salad/makedoc.py
@@ -1,5 +1,6 @@
 import argparse
 import copy
+import html
 import logging
 import os
 import re
@@ -7,6 +8,7 @@
 from io import StringIO, TextIOWrapper
 from typing import (
     IO,
+    TYPE_CHECKING,
     Any,
     Dict,
     List,
@@ -19,13 +21,21 @@
 )
 from urllib.parse import urldefrag
 
-import mistune
+from mistune import create_markdown
+from mistune.renderers import HTMLRenderer
+from mistune.util import escape_html
 
 from .exceptions import SchemaSaladException, ValidationException
 from .schema import avro_field_name, extend_and_specialize, get_metaschema
 from .utils import add_dictlist, aslist
 from .validate import avro_type_name
 
+if TYPE_CHECKING:
+    # avoid import error since typing stubs do not exist in actual package
+    from mistune import State
+    from mistune.markdown import Markdown
+    from mistune.plugins import PluginName
+
 _logger = logging.getLogger("salad")
 
 
@@ -57,23 +67,208 @@ def linkto(item: str) -> str:
     return f"[{frg}](#{to_id(frg)})"
 
 
-class MyRenderer(mistune.Renderer):
-    def __init__(self) -> None:
-        super().__init__()
-        self.options = {}
+class MyRenderer(HTMLRenderer):
+    """Custom renderer with different representations of selected HTML tags."""
 
-    def header(self, text: str, level: int, raw: Optional[Any] = None) -> str:
+    def heading(self, text: str, level: int) -> str:
+        """Override HTML heading creation with text IDs."""
         return (
             """<h{} id="{}" class="section">{} <a href="#{}">&sect;</a></h{}>""".format(
                 level, to_id(text), text, to_id(text), level
             )
         )
 
-    def table(self, header: str, body: str) -> str:
-        return (
-            '<table class="table table-striped">\n<thead>{}</thead>\n'
-            "<tbody>\n{}</tbody>\n</table>\n"
-        ).format(header, body)
+    def text(self, text: str) -> str:
+        """Don't escape quotation marks."""
+        # avoid convert of & if already escaped
+        text = html.unescape(text)
+        # html.escape does both single/double quotes ('/")
+        # mistune.util.escape does only double quotes
+        return html.escape(text, quote=self._escape)
+
+    def inline_html(self, html: str) -> str:
+        """Don't escape characters in predefined HTML within paragraph tags."""
+        return html + "\n"
+
+    def block_html(self, html: str) -> str:
+        """Don't escape characters nor wrap predefined HTML within paragraph tags."""
+        return html + "\n"
+
+    def block_code(self, code: str, info: Optional[str] = None) -> str:
+        """Don't escape quotation marks."""
+        text = "<pre><code"
+        if info is not None:
+            info = info.strip()
+        if info:
+            lang = info.split(None, 1)[0]
+            lang = escape_html(lang)
+            text += ' class="language-' + lang + '"'
+        return text + ">" + html.escape(code, quote=self._escape) + "</code></pre>\n"
+
+
+def markdown_list_hook(markdown, text, state):
+    # type: (Markdown[str, Any], str, State) -> Tuple[str, State]
+    """Patches problematic Markdown lists for later HTML generation.
+
+    When a Markdown list with paragraphs not indented with the list
+    markers (no spaces before following lines), ``mistune`` v2 does
+    not handle them correctly. This is however permitted as per
+    https://daringfireball.net/projects/markdown/syntax#list
+
+    For example:
+
+    ```markdown
+    * some list
+    * item with
+    paragraph
+    * other item
+    ```
+
+    Similarly, lists that are completely indented or that contains nested lists
+    produce incorrect HTML ``<p>``/``<li>`` tag combinations.
+
+    Because list parsing is deeply nested within ``mistune.block_parser.BlockParser``
+    and that there is no easy way to override utility functions it employs to adjust
+    patterns of list items without reimplementing it or a lot of monkey patching,
+    instead catch the problem cases before rendering and adjust them with a hook.
+
+    See https://github.com/lepture/mistune/issues/296
+    and https://github.com/common-workflow-language/schema_salad/pull/619
+    """
+    pattern = re.compile(
+        r"^"
+        r"(?P<before>\n*)"  # detect newline to start capture on bullet line
+        r"(?P<indent>\s*)"
+        r"(?P<bullet>[0-9]+[.)]?|[*-])"
+        r"(?P<spacing>\s+)"
+        r"(?P<first_line>.*)"
+        # if more than one empty line is found, end search (paragraph after list)
+        r"(?!\n\s*\n\s*)"
+        # otherwise, find all lines part of the same bullet item
+        # if this bullet item is indented (nested list), match indents to collect
+        # use negative lookahead to avoid over capturing following bullets
+        r"(?P<other_lines>(?:\n\s*(?![0-9]+[.)]+|[*-])(?P=indent).*"
+        r"(?!\n\s*\n\s*)"  # avoid match past list on last indented line
+        r")+)*"  # end 'other_lines'
+        # because of negative lookahead logic, there is sometimes a remaining
+        # trailing character to capture on the last list item line
+        r"(?P<remain>.*)",
+        re.MULTILINE,
+    )
+    matches = list(re.finditer(pattern, text))
+    if not matches:
+        return text, state
+    result = ""
+    begin = 0
+    for match in matches:
+        start, end = match.start(), match.end()
+        start += len(match.group("before"))
+        result += text[begin:start]  # add text in between matched lists
+
+        # process and indented list (de-indent, apply fixes, and re-indent)
+        indent_prefix = match.group("indent")
+        if indent_prefix:
+            intend_list = text[start:end]
+            intend_list = "\n".join(
+                [
+                    line.strip()
+                    for line in intend_list.split("\n")
+                    # mistune is having trouble understanding list items
+                    # if they are separated by an additional line in between
+                    # ignore empty lines to group items together,
+                    # avoiding split into distinct list after injecting <p> tags
+                    if line.strip()
+                ]
+            )
+            intend_list, _ = markdown_list_hook(markdown, intend_list, state)
+            # remove final newline from other if/else branch
+            intend_list = intend_list.rstrip()
+            intend_list = "\n".join(
+                [indent_prefix + line for line in intend_list.split("\n")]
+            )
+            result += intend_list + "\n"
+        # process a plain list
+        # pad extra spaces to multi-lines items contents after bullet
+        else:
+            item = (
+                match.group("indent") + match.group("bullet") + match.group("spacing")
+            )
+            result += item + match.group("first_line")
+            indent = (
+                "\n"
+                + match.group("indent").split("\n")[-1]
+                + (" " * len(match.group("bullet")))
+                + match.group("spacing")
+            )
+            other = match.group("other_lines")
+            if other:
+                other = indent.join(
+                    [
+                        line.strip()
+                        for line in other.split("\n")
+                        # mistune is having trouble understanding list items
+                        # if they are separated by an additional line in between
+                        # ignore empty lines to group items together,
+                        # avoiding split into distinct list after injecting <p> tags
+                        if line.strip()
+                    ]
+                )
+                # Add a single space to ensure words remain separated.
+                # If we use newline/indent like in other lines above, mistune
+                # splits the items into 2 lists. Although technically the Markdown
+                # will have an extra space, spacing will be patched when generating
+                # the HTML whether newline or space was used.
+                if (
+                    # only apply the extra space if items are actually
+                    # 2 words to avoid incorrect split of compound words
+                    # (e.g.: "key-value", not "key- value").
+                    re.match(r".*[^-]$", result[-2:], re.I)
+                    and re.match(r"^[^-].*", other[:2], re.I)
+                ):
+                    result += " "
+                result += other
+            result += match.group("remain") + "\n"
+
+        begin = end + 1
+    result += text[begin:]
+    # Because lists regexes are designed to detect line-by-line bullets/paragraphs,
+    # we cannot directly (or easily / with certainty) detect "list-like" encase in
+    # fenced code definitions that could be much above/below the "list-like" items.
+    # Instead, simply revert them after the fact with document-level matches of fenced codes.
+    _logger.debug("Original Markdown:\n\n%s\n\n", text)
+    _logger.debug("Modified Markdown:\n\n%s\n\n", result)
+    result = patch_fenced_code(text, result)
+    _logger.debug("Patched Markdown:\n\n%s\n\n", result)
+    return result, state
+
+
+def patch_fenced_code(original_markdown_text: str, modified_markdown_text: str) -> str:
+    """
+    Reverts fenced code fragments found in the modified contents back to their original definition.
+    """
+    # Pattern inspired from 'mistune.block_parser.BlockParser.FENCED_CODE'.
+    # However, instead of the initial ' {0,3}' part to match any indented fenced-code,
+    # use any quantity of spaces, as long as they match at the end as well (using '\1').
+    # Because of nested fenced-code in lists, it can be more indented than "normal".
+    fenced_code_pattern = re.compile(
+        r"( *)(`{3,}|~{3,})([^`\n]*)\n(?:|([\s\S]*?)\n)(?:\1\2[~`]* *\n+|$)"
+    )
+    matches_original = list(re.finditer(fenced_code_pattern, original_markdown_text))
+    matches_modified = list(re.finditer(fenced_code_pattern, modified_markdown_text))
+    if len(matches_original) != len(matches_modified):
+        raise ValueError(
+            "Cannot patch fenced code definitions with inconsistent matches."
+        )
+    result = ""
+    begin = 0
+    for original, modified in zip(matches_original, matches_modified):
+        ori_s, ori_e = original.start(), original.end()
+        mod_s, mod_e = modified.start(), modified.end()
+        result += modified_markdown_text[begin:mod_s]  # add text in between matches
+        result += original_markdown_text[ori_s:ori_e]  # revert the fenced code
+        begin = mod_e  # skip over the modified fenced code for next match
+    result += modified_markdown_text[begin:]  # left over text after last match
+    return result
 
 
 def to_id(text: str) -> str:
@@ -422,8 +617,22 @@ def extendsfrom(item: Dict[str, Any], ex: List[Dict[str, Any]]) -> None:
             f["doc"] = number_headings(self.toc, f["doc"])
 
         doc = doc + "\n\n" + f["doc"]
-
-        doc = mistune.markdown(doc, renderer=MyRenderer())
+        plugins = [
+            "strikethrough",
+            "footnotes",
+            "table",
+            "url",
+        ]  # type: List[PluginName]  # fix error Generic str != explicit Literals
+        # if escape active, wraps literal HTML into '<p> {HTML} </p>'
+        # we must pass it to both since 'MyRenderer' is predefined
+        escape = False
+        markdown2html = create_markdown(
+            renderer=MyRenderer(escape=escape),
+            plugins=plugins,
+            escape=escape,
+        )  # type: Markdown[str, Any]
+        markdown2html.before_parse_hooks.append(markdown_list_hook)
+        doc = markdown2html(doc)
 
         if f["type"] == "record":
             doc += "<h3>Fields</h3>"
@@ -463,7 +672,7 @@ def extendsfrom(item: Dict[str, Any], ex: List[Dict[str, Any]]) -> None:
                     self.typefmt(
                         tp, self.redirects, jsonldPredicate=i.get("jsonldPredicate")
                     ),
-                    mistune.markdown(desc),
+                    markdown2html(desc),
                 )
                 if opt:
                     required.append(tr)
diff --git a/schema_salad/metaschema/map_res_proc.yml b/schema_salad/metaschema/map_res_proc.yml
index 52e9c220..704c7bbb 100644
--- a/schema_salad/metaschema/map_res_proc.yml
+++ b/schema_salad/metaschema/map_res_proc.yml
@@ -9,4 +9,4 @@
             "key": "shaggy"
         }
     ]
-}
\ No newline at end of file
+}
diff --git a/schema_salad/metaschema/map_res_src.yml b/schema_salad/metaschema/map_res_src.yml
index 9df0c356..e6e3e0bd 100644
--- a/schema_salad/metaschema/map_res_src.yml
+++ b/schema_salad/metaschema/map_res_src.yml
@@ -5,4 +5,4 @@
     },
     "fred": "daphne"
   }
-}
\ No newline at end of file
+}
diff --git a/setup.py b/setup.py
index 84c86adb..58d3a1f5 100644
--- a/setup.py
+++ b/setup.py
@@ -87,7 +87,7 @@
     "ruamel.yaml >= 0.16.12, < 0.17.22",
     "rdflib >= 4.2.2, < 7.0.0",
     "rdflib-jsonld>=0.4.0, <= 0.6.1;python_version<='3.6'",
-    "mistune >= 0.8.1, < 0.9",
+    "mistune>=2.0.3,<2.1",
     "CacheControl[filecache] >= 0.11.7, < 0.13",
 ]
 


Test patch:
diff --git a/schema_salad/tests/inherited-attributes.yml b/schema_salad/tests/inherited-attributes.yml
index d37a827c..2e8f9788 100644
--- a/schema_salad/tests/inherited-attributes.yml
+++ b/schema_salad/tests/inherited-attributes.yml
@@ -15,6 +15,16 @@
 - name: Child
   doc: |
     Child record
+    <p>
+    <table class="table">
+    <tr><td>symbol::             </td><td>{Unicode alphanumeric}+</td></tr>
+    <tr><td>singleq::            </td><td>[' (( {character - { | \ ' \} } ))* ']</td></tr>
+    <tr><td>doubleq::            </td><td>[" (( {character - { | \ " \} } ))* "]</td></tr>
+    <tr><td>index::              </td><td>[ {decimal digit}+ ]</td></tr>
+    <tr><td>segment::            </td><td>. {symbol} | {singleq} | {doubleq} | {index}</td></tr>
+    <tr><td>parameter reference::</td><td>$( {symbol} {segment}*)</td></tr>
+    </table>
+    </p>
   type: record
   extends: Parent
   fields:
diff --git a/schema_salad/tests/metaschema-pre.yml b/schema_salad/tests/metaschema-pre.yml
index 9325d37e..a10e5f23 100644
--- a/schema_salad/tests/metaschema-pre.yml
+++ b/schema_salad/tests/metaschema-pre.yml
@@ -36,9 +36,9 @@
             "## Identifier maps\n\nThe schema may designate certain fields as having a `mapSubject`.  If the\nvalue of the field is a JSON object, it must be transformed into an array of\nJSON objects.  Each key-value pair from the source JSON object is a list\nitem, each list item must be a JSON object, and the value of the key is\nassigned to the field specified by `mapSubject`.\n\nFields which have `mapSubject` specified may also supply a `mapPredicate`.\nIf the value of a map item is not a JSON object, the item is transformed to a\nJSON object with the key assigned to the field specified by `mapSubject` and\nthe value assigned to the field specified by `mapPredicate`.\n\n### Identifier map example\n\nGiven the following schema:\n\n```\n",
             "{\n  \"$graph\": [{\n    \"name\": \"MappedType\",\n    \"type\": \"record\",\n    \"documentRoot\": true,\n    \"fields\": [{\n      \"name\": \"mapped\",\n      \"type\": {\n        \"type\": \"array\",\n        \"items\": \"ExampleRecord\"\n      },\n      \"jsonldPredicate\": {\n        \"mapSubject\": \"key\",\n        \"mapPredicate\": \"value\"\n      }\n    }],\n  },\n  {\n    \"name\": \"ExampleRecord\",\n    \"type\": \"record\",\n    \"fields\": [{\n      \"name\": \"key\",\n      \"type\": \"string\"\n      }, {\n      \"name\": \"value\",\n      \"type\": \"string\"\n      }\n    ]\n  }]\n}\n",
             "```\n\nProcess the following example:\n\n```\n",
-            "{\n  \"mapped\": {\n    \"shaggy\": {\n      \"value\": \"scooby\"\n    },\n    \"fred\": \"daphne\"\n  }\n}",
+            "{\n  \"mapped\": {\n    \"shaggy\": {\n      \"value\": \"scooby\"\n    },\n    \"fred\": \"daphne\"\n  }\n}\n",
             "```\n\nThis becomes:\n\n```\n",
-            "{\n    \"mapped\": [\n        {\n            \"value\": \"daphne\",\n            \"key\": \"fred\"\n        },\n        {\n            \"value\": \"scooby\",\n            \"key\": \"shaggy\"\n        }\n    ]\n}",
+            "{\n    \"mapped\": [\n        {\n            \"value\": \"daphne\",\n            \"key\": \"fred\"\n        },\n        {\n            \"value\": \"scooby\",\n            \"key\": \"shaggy\"\n        }\n    ]\n}\n",
             "```\n",
             "## Domain Specific Language for types\n\nFields may be tagged `typeDSL: true` in `jsonldPredicate`.  If so, the field is expanded using the\nfollowing micro-DSL for schema salad types:\n\n* If the type ends with a question mark `?`, the question mark is stripped off and the type is expanded to a union with `null`\n* If the type ends with square brackets `[]` it is expanded to an array with items of the preceding type symbol\n* The type may end with both `[]?` to indicate it is an optional array.\n* Identifier resolution is applied after type DSL expansion.\n\n### Type DSL example\n\nGiven the following schema:\n\n```\n",
             "{\n  \"$graph\": [\n  {\"$import\": \"metaschema_base.yml\"},\n  {\n    \"name\": \"TypeDSLExample\",\n    \"type\": \"record\",\n    \"documentRoot\": true,\n    \"fields\": [{\n      \"name\": \"extype\",\n      \"type\": \"string\",\n      \"jsonldPredicate\": {\n        _type: \"@vocab\",\n        \"typeDSL\": true\n      }\n    }]\n  }]\n}\n",
diff --git a/schema_salad/tests/test_makedoc.py b/schema_salad/tests/test_makedoc.py
index 41fa8d63..28d62b50 100644
--- a/schema_salad/tests/test_makedoc.py
+++ b/schema_salad/tests/test_makedoc.py
@@ -1,6 +1,25 @@
-"""Test schema-salad makedoc"""
+"""
+Test schema-salad-doc.
 
+(also known as schema-salad-tool --print-doc)
+
+For convenience, tests are checking exact strings. In the event of changes in
+the "mistune" package, makedoc.py, or other changes, feel free to modify the test
+strings as long as the new HTML renders the same way in typical browsers.
+
+Likewise, if the schema-salad metaschema changes and it is missing one or more
+of the features tested below, then please copy those old features to a new file
+and update the affected tests to use those new file(s).
+"""
+import hashlib
+import inspect
+import json
+from pathlib import Path
 from io import StringIO
+from typing import Optional
+
+import tempfile
+import pytest
 
 from schema_salad.makedoc import makedoc
 
@@ -16,3 +35,215 @@ def test_schema_salad_inherit_docs() -> None:
 
     # The parent ID documentation (i.e. Parent ID) must appear exactly once.
     assert 1 == stdout.getvalue().count("Parent ID")
+
+
+def generate_doc(schema_data: Optional[str] = None) -> str:
+    """Avoid error when calling fixture directly."""
+    stdout = StringIO()
+    if schema_data:
+        with tempfile.NamedTemporaryFile(
+            mode="w", encoding="utf-8", suffix=".yml"
+        ) as tmp_file:
+            tmp_file.write(schema_data)
+            tmp_file.flush()
+            tmp_file.seek(0)
+            makedoc(stdout, tmp_file.name)
+    else:
+        schema_path = get_data("metaschema/metaschema.yml")
+        assert schema_path
+        makedoc(stdout, schema_path)
+    return stdout.getvalue()
+
+
+@pytest.fixture(scope="session", name="metaschema_doc")
+def fixture_metaschema_doc() -> str:
+    """Pytest Fixture of the rendered HTML for the metaschema schema."""
+    return generate_doc()
+
+
+def test_doc_fenced_code_contents_preserved() -> None:
+    """
+    Validates that fenced code contents are not interpreted as Markdown definitions and converted into erroneous HTML.
+
+    An example of problem case is when a definition looks like a Markdown list (e.g.: a YAML array).
+    It must not be converted into HTML contents with list tags.
+    However, special characters (e.g.: ``<``, ``>``) must still be escaped, otherwise they will not be correctly
+    rendered within an HTML ``<pre><code>`` block.
+    """
+    data = inspect.cleandoc(
+        """
+        Option one generic example:
+        ```
+        some_cwl_field:
+          - key_field: a_complex_type1
+            field2: foo
+            field3: bar
+          - key_field: a_complex_type2
+            field2: foo2
+            field3: bar2
+          - key_field: a_complex_type3
+        ```
+
+        Option one generic example:
+        ```
+        inputs:
+          - id: workflow_input01
+            type: string
+          - id: workflow_input02
+            type: File
+            format: http://edamontology.org/format_2572
+        ```
+
+        Special Characters:
+        ```
+        data:
+          - test: value 1 < 2 is true
+          - test: value 2 > 1 is false
+        ```
+        """
+    )
+    schema_data = json.dumps(
+        [
+            {
+                "name": "https://w3id.org/cwl/salad#test",
+                "type": "documentation",
+                "doc": data,
+            }
+        ]
+    )
+    schema_doc = generate_doc(schema_data)
+    schema_doc = schema_doc.split("</head>")[-1]  # just for nicer/readable assert diffs
+    assert (
+        "<p>Option one generic example:</p>\n"
+        "<pre><code>some_cwl_field:\n"
+        "  - key_field: a_complex_type1\n"
+        "    field2: foo\n"
+        "    field3: bar\n"
+        "  - key_field: a_complex_type2\n"
+        "    field2: foo2\n"
+        "    field3: bar2\n"
+        "  - key_field: a_complex_type3\n"
+        "</code></pre>\n"
+    ) in schema_doc
+    assert (
+        "<p>Option one generic example:</p>\n"
+        "<pre><code>inputs:\n"
+        "  - id: workflow_input01\n"
+        "    type: string\n"
+        "  - id: workflow_input02\n"
+        "    type: File\n"
+        "    format: http://edamontology.org/format_2572\n"
+        "</code></pre>\n"
+    ) in schema_doc
+    assert (
+        "<p>Special Characters:</p>\n"
+        "<pre><code>data:\n"
+        "  - test: value 1 &lt; 2 is true\n"
+        "  - test: value 2 &gt; 1 is false\n"
+        "</code></pre>\n"
+    ) in schema_doc
+
+
+def test_doc_headings_target_anchor(metaschema_doc: str) -> None:
+    """Doc headers must have an id and section link."""
+    assert (
+        '<h1 id="Abstract" class="section">Abstract '
+        '<a href="#Abstract">&sect;</a></h1>' in metaschema_doc
+    )
+
+
+def test_doc_render_table_of_contents(metaschema_doc: str) -> None:
+    """The special Table of Contents token must be replaced with a rendered table."""
+    assert "!--ToC--" not in metaschema_doc
+
+
+def test_plain_links_autolinked(metaschema_doc: str) -> None:
+    """Plan links should be treated as if they were wrapped in angle brackets."""
+    assert (
+        "This document is the product of the "
+        '<a href="https://groups.google.com/forum/#!forum/common-workflow-language">'
+        "Common Workflow Language working\ngroup</a>" in metaschema_doc
+    )
+
+
+def test_embedded_html_unescaped() -> None:
+    """Raw HTML shouldn't get escaped."""
+    schema_path = get_data("tests/inherited-attributes.yml")
+    assert schema_path
+    stdout = StringIO()
+    makedoc(stdout, schema_path)
+    html = stdout.getvalue()
+
+    assert '<table class="table">' in html
+    assert "&lt;table class=&quot;table&quot;&gt;" not in html
+
+
+def test_multiline_list_entries_word_spacing(metaschema_doc: str) -> None:
+    """Hanging indents in Markdown lists don't lead to wordsmushing."""
+    assert "as itis poorly documented" not in metaschema_doc
+    assert "base URI for the document used toresolve relative" not in metaschema_doc
+    assert "The keys ofthe object are namespace prefixes" not in metaschema_doc
+    assert (
+        "This field may list URIreferences to documents in RDF-XML"
+        not in metaschema_doc
+    )
+    assert "defines valid fields thatmake up a record type" not in metaschema_doc
+    assert "set of symbols that arevalid value" not in metaschema_doc
+
+
+def test_multiline_list_entries_without_indention(metaschema_doc: str) -> None:
+    """Hanging indents are not required in Markdown lists."""
+    # See https://daringfireball.net/projects/markdown/syntax#list
+    # and https://spec.commonmark.org/0.30/#example-290
+    # Some newlines in markdown are replaced by spaces purposely
+    # to avoid invalid Markdown to HTML conversion as preserve words separated.
+    assert (
+        "<li><p>At least one record definition object which defines valid fields that "  # purposely no \n
+        "make up a record type.  Record field definitions include the valid types\n"
+        "that may be assigned to each field and annotations to indicate fields\n"
+        'that represent identifiers and links, described below in "Semantic\n'
+        'Annotations".</p>\n'
+        "</li>\n"
+        "<li><p>Any number of enumerated type objects which define a set of finite "
+        "set of symbols that are "  # purposely no \n
+        "valid value of the type.</p>\n"
+        "</li>\n"
+        "<li><p>Any number of documentation objects which allow in-line "
+        "documentation of the schema.</p>\n"
+        "</li>" in metaschema_doc
+    )
+    assert (
+        "<li>At least one record definition object which defines valid fields that</li>"
+        not in metaschema_doc
+    )
+
+
+def test_detect_changes_in_html(metaschema_doc: str, tmp_path: Path) -> None:
+    """Catch all for changes in HTML output, please adjust if the changes are innocent."""
+    # If the hash changed because the metaschema itself changed (without changes
+    # to makedoc.py or the version of mistune) then you can directly update the
+    # hash value below.
+    #
+    # Otherwise, follow this procedure verify that the changed HTML rendering
+    # is acceptable (or use make 'check-metaschema-diff' and 'compute-metaschema-hash'):
+    # 1. Render the metaschema schema into using an older, known-working version
+    #    of schema-salad:
+    #    `schema-salad-doc schema_salad/metaschema/metaschema.yml > /tmp/metaschema.orig.html`
+    # 2. Render the metaschema schema into using proposed changed codebase
+    #    `schema-salad-doc schema_salad/metaschema/metaschema.yml > /tmp/metaschema.new.html`
+    # 3. Confirm the other tests in this file pass using the new code/mistune,
+    #    adjusting the test strings if the changes are truly innocent.
+    # 4. Check the `diff` between the saved HTML pages to check for obvious problems
+    #    `diff /tmp/metaschema.orig.html /tmp/metaschema.new.html`
+    # 5. Check the HTML in both Firefox and Chrome, especially near areas
+    #    of differences in the diff
+    # 6. If the changes are agreeable, then update the hash below
+    hasher = hashlib.sha256()
+    hasher.update(metaschema_doc.encode("utf-8"))
+    result = tmp_path / "result.html"
+    with open(result, "w") as h:
+        h.write(metaschema_doc)
+    assert (
+        hasher.hexdigest()
+        == "0dec6861574d2a80f5f5cd177103747e893fe6b23d1ec9b2b37850408d9ca1d1"
+    ), result
diff --git a/schema_salad/tests/test_ref_resolver.py b/schema_salad/tests/test_ref_resolver.py
index 78821e9b..3d62160c 100644
--- a/schema_salad/tests/test_ref_resolver.py
+++ b/schema_salad/tests/test_ref_resolver.py
@@ -181,7 +181,7 @@ def test_import_list() -> None:
 
 
 def test_fetch_inject_id() -> None:
-    path = get_data("schema_salad/tests/inject-id1.yml")
+    path = get_data("tests/inject-id1.yml")
     assert path
     if is_fs_case_sensitive(os.path.dirname(path)):
 
@@ -202,7 +202,7 @@ def lower(item: str) -> str:
     )
 
     l2 = Loader({"id": "@id"})
-    path2 = get_data("schema_salad/tests/inject-id2.yml")
+    path2 = get_data("tests/inject-id2.yml")
     assert path2
     furi2 = file_uri(path2)
     r2, _ = l2.resolve_ref(furi2)
@@ -210,7 +210,7 @@ def lower(item: str) -> str:
     assert [lower(furi2)] == sorted(list(lower(k) for k in l2.idx.keys()))
 
     l3 = Loader({"id": "@id"})
-    path3 = get_data("schema_salad/tests/inject-id3.yml")
+    path3 = get_data("tests/inject-id3.yml")
     assert path3
     furi3 = file_uri(path3)
     r3, _ = l3.resolve_ref(furi3)
@@ -221,7 +221,7 @@ def lower(item: str) -> str:
 
 
 def test_attachments() -> None:
-    path = get_data("schema_salad/tests/multidoc.yml")
+    path = get_data("tests/multidoc.yml")
     assert path
     furi = file_uri(path)
 
diff --git a/test-requirements.txt b/test-requirements.txt
index e35449e1..a0894153 100644
--- a/test-requirements.txt
+++ b/test-requirements.txt
@@ -1,3 +1,4 @@
 pytest >= 6.2, < 7.3
 pytest-cov
 pytest-xdist
+tox