ui: render markdown in release notes (#22754)
* convert release notes from markdown to html * fall back to previous behavior if utf8 decoding or markdown parsing throws * make simple markdown parser to avoid needing a library * add unit test * move markdown parser to common. add unit test use `markdown-it-py` instead of `markdown` dependency for test comparison since it's already in Pipfile.lock * test (almost) all release notes and add some extra html encoding * update lock Co-authored-by: Willem Melching <willem.melching@gmail.com>pull/22795/head
parent
1289ebe9bd
commit
1aebe6ff6e
1
Pipfile
1
Pipfile
|
@ -42,6 +42,7 @@ jupyterlab = "*"
|
||||||
jupyterlab-vim = "*"
|
jupyterlab-vim = "*"
|
||||||
keras_applications = "*"
|
keras_applications = "*"
|
||||||
lru-dict = "*"
|
lru-dict = "*"
|
||||||
|
markdown-it-py = "*"
|
||||||
matplotlib = "*"
|
matplotlib = "*"
|
||||||
mock = "*"
|
mock = "*"
|
||||||
mpld3 = "*"
|
mpld3 = "*"
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"_meta": {
|
"_meta": {
|
||||||
"hash": {
|
"hash": {
|
||||||
"sha256": "7d947b4ec704dbcb974423ac826df3222d956ffd608b989a3ab607e8d25df484"
|
"sha256": "bda9b34e8525624fd39ac3a78476942df82d79dd1e06555cbd1e38a6c8574976"
|
||||||
},
|
},
|
||||||
"pipfile-spec": 6,
|
"pipfile-spec": 6,
|
||||||
"requires": {
|
"requires": {
|
||||||
|
@ -2287,7 +2287,7 @@
|
||||||
"sha256:36be6bb3ad987bfdb839f5ba78ddf094552ca38ccbd784ae4f74a4e1419fc6e3",
|
"sha256:36be6bb3ad987bfdb839f5ba78ddf094552ca38ccbd784ae4f74a4e1419fc6e3",
|
||||||
"sha256:98080fc0bc34c4f2bcf0846a096a9429acbd9d5d8e67ed34026c03c61c464389"
|
"sha256:98080fc0bc34c4f2bcf0846a096a9429acbd9d5d8e67ed34026c03c61c464389"
|
||||||
],
|
],
|
||||||
"markers": "python_version ~= '3.6'",
|
"index": "pypi",
|
||||||
"version": "==1.1.0"
|
"version": "==1.1.0"
|
||||||
},
|
},
|
||||||
"markupsafe": {
|
"markupsafe": {
|
||||||
|
|
|
@ -0,0 +1,48 @@
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
HTML_REPLACEMENTS = [
|
||||||
|
(r'&', r'&'),
|
||||||
|
(r'"', r'"'),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def parse_markdown(text: str, tab_length: int = 2) -> str:
|
||||||
|
lines = text.split("\n")
|
||||||
|
output: List[str] = []
|
||||||
|
list_level = 0
|
||||||
|
|
||||||
|
def end_outstanding_lists(level: int, end_level: int) -> int:
|
||||||
|
while level > end_level:
|
||||||
|
level -= 1
|
||||||
|
output.append("</ul>")
|
||||||
|
if level > 0:
|
||||||
|
output.append("</li>")
|
||||||
|
return end_level
|
||||||
|
|
||||||
|
for i, line in enumerate(lines):
|
||||||
|
if i + 1 < len(lines) and lines[i + 1].startswith("==="): # heading
|
||||||
|
output.append(f"<h1>{line}</h1>")
|
||||||
|
elif line.startswith("==="):
|
||||||
|
pass
|
||||||
|
elif line.lstrip().startswith("* "): # list
|
||||||
|
line_level = 1 + line.count(" " * tab_length, 0, line.index("*"))
|
||||||
|
if list_level >= line_level:
|
||||||
|
list_level = end_outstanding_lists(list_level, line_level)
|
||||||
|
else:
|
||||||
|
list_level += 1
|
||||||
|
if list_level > 1:
|
||||||
|
output[-1] = output[-1].replace("</li>", "")
|
||||||
|
output.append("<ul>")
|
||||||
|
output.append(f"<li>{line.replace('*', '', 1).lstrip()}</li>")
|
||||||
|
else:
|
||||||
|
list_level = end_outstanding_lists(list_level, 0)
|
||||||
|
if len(line) > 0:
|
||||||
|
output.append(line)
|
||||||
|
|
||||||
|
end_outstanding_lists(list_level, 0)
|
||||||
|
output_str = "\n".join(output) + "\n"
|
||||||
|
|
||||||
|
for (fr, to) in HTML_REPLACEMENTS:
|
||||||
|
output_str = output_str.replace(fr, to)
|
||||||
|
|
||||||
|
return output_str
|
|
@ -0,0 +1,26 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
from markdown_it import MarkdownIt
|
||||||
|
import os
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from common.basedir import BASEDIR
|
||||||
|
from common.markdown import parse_markdown
|
||||||
|
|
||||||
|
|
||||||
|
class TestMarkdown(unittest.TestCase):
|
||||||
|
# validate that our simple markdown parser produces the same output as `markdown_it` from pip
|
||||||
|
def test_current_release_notes(self):
|
||||||
|
self.maxDiff = None
|
||||||
|
|
||||||
|
with open(os.path.join(BASEDIR, "RELEASES.md")) as f:
|
||||||
|
for r in f.read().split("\n\n"):
|
||||||
|
|
||||||
|
# No hyperlink support is ok
|
||||||
|
if '[' in r:
|
||||||
|
continue
|
||||||
|
|
||||||
|
self.assertEqual(MarkdownIt().render(r), parse_markdown(r))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
|
@ -53,7 +53,7 @@ class TestUpdated(unittest.TestCase):
|
||||||
f"cd {self.basedir} && scons -j{os.cpu_count()} cereal/ common/"
|
f"cd {self.basedir} && scons -j{os.cpu_count()} cereal/ common/"
|
||||||
])
|
])
|
||||||
|
|
||||||
self.params = Params(db=os.path.join(self.basedir, "persist/params"))
|
self.params = Params(os.path.join(self.basedir, "persist/params"))
|
||||||
self.params.clear_all()
|
self.params.clear_all()
|
||||||
os.sync()
|
os.sync()
|
||||||
|
|
||||||
|
|
|
@ -35,6 +35,7 @@ from pathlib import Path
|
||||||
from typing import List, Tuple, Optional
|
from typing import List, Tuple, Optional
|
||||||
|
|
||||||
from common.basedir import BASEDIR
|
from common.basedir import BASEDIR
|
||||||
|
from common.markdown import parse_markdown
|
||||||
from common.params import Params
|
from common.params import Params
|
||||||
from selfdrive.hardware import EON, TICI, HARDWARE
|
from selfdrive.hardware import EON, TICI, HARDWARE
|
||||||
from selfdrive.swaglog import cloudlog
|
from selfdrive.swaglog import cloudlog
|
||||||
|
@ -113,9 +114,11 @@ def set_params(new_version: bool, failed_count: int, exception: Optional[str]) -
|
||||||
if new_version:
|
if new_version:
|
||||||
try:
|
try:
|
||||||
with open(os.path.join(FINALIZED, "RELEASES.md"), "rb") as f:
|
with open(os.path.join(FINALIZED, "RELEASES.md"), "rb") as f:
|
||||||
r = f.read()
|
r = f.read().split(b'\n\n', 1)[0] # Slice latest release notes
|
||||||
r = r[:r.find(b'\n\n')] # Slice latest release notes
|
try:
|
||||||
params.put("ReleaseNotes", r + b"\n")
|
params.put("ReleaseNotes", parse_markdown(r.decode("utf-8")))
|
||||||
|
except Exception:
|
||||||
|
params.put("ReleaseNotes", r + b"\n")
|
||||||
except Exception:
|
except Exception:
|
||||||
params.put("ReleaseNotes", "")
|
params.put("ReleaseNotes", "")
|
||||||
params.put_bool("UpdateAvailable", True)
|
params.put_bool("UpdateAvailable", True)
|
||||||
|
|
Loading…
Reference in New Issue