Skip to content

Commit d0aa918

Browse files
committed
v0.0.9
1 parent f0308d2 commit d0aa918

File tree

6 files changed

+264
-7
lines changed

6 files changed

+264
-7
lines changed

build/lib/httpmdhtml/__init__.py

Whitespace-only changes.

build/lib/httpmdhtml/md_to_html.py

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
"""
2+
just convert a markdown file to HTML and write it out to a file
3+
"""
4+
import os
5+
import sys
6+
import base64
7+
from pathlib import Path
8+
import argparse
9+
import re
10+
from markdown_it import MarkdownIt
11+
from bs4 import BeautifulSoup, element
12+
13+
14+
def markdown_to_html(
15+
MarkdownIt_obj,
16+
in_file_path,
17+
out_file_path="tmp.html",
18+
encode_local_images=False,
19+
css_file=None,
20+
live_md_rr=None,
21+
):
22+
text = open(in_file_path, "r").read()
23+
tokens = MarkdownIt_obj.parse(text)
24+
html_text = MarkdownIt_obj.render(text)
25+
# pretty CSS
26+
soup = BeautifulSoup(html_text, "html5lib") # adds <html>, <head>, <body>
27+
soup.select_one("head").append(soup.new_tag("meta"))
28+
soup.select_one("meta").attrs["charset"] = "UTF-8"
29+
# if lots of images, caching is preferable more often than not
30+
# soup.select_one('meta').attrs['http-equiv'] = "Cache-control"
31+
# soup.select_one('meta').attrs['content'] = "no-cache"
32+
soup.select_one("head").append(soup.new_tag("style"))
33+
if css_file:
34+
if css_file == "none":
35+
css = ""
36+
else:
37+
with open(css_file, "r") as f:
38+
css = f.read()
39+
else: # #272822
40+
css = """
41+
body { background-color: #272822; color: #e6edf3; font-family: -apple-system,BlinkMacSystemFont,"Segoe UI","Noto Sans",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji"; position: relative; max-width: 960px; margin: auto }
42+
a[href] { color: #66d9ef; }
43+
code { color: #e6edf3; font-family: monospace; white-space: break-spaces; }
44+
p code { padding: .2em .4em; border-radius: 6px; background-color: #343941; }
45+
pre { padding: 1em; border-radius: 6px; background-color: #161b22; }
46+
table, th, td { border: 1px solid; border-collapse: collapse; padding-left: 4px; padding-right: 4px; }
47+
img { max-width: 100%; }
48+
"""
49+
soup.select_one("style").string = css
50+
if live_md_rr:
51+
script = (
52+
f"setTimeout(function(){{ document.location.reload(); }}, {live_md_rr});"
53+
)
54+
soup.select_one("head").append(soup.new_tag("script"))
55+
soup.select_one("script").string = script
56+
if encode_local_images:
57+
img_elems = soup.select("img")
58+
url_pattern = "^https?:\\/\\/(?:www\\.)?[-a-zA-Z0-9@:%._\\+~#=]{1,256}\\.[a-zA-Z0-9()]{1,6}\\b(?:[-a-zA-Z0-9()@:%_\\+.~#?&\\/=]*)$"
59+
for img in img_elems:
60+
if not re.match(url_pattern, img.attrs["src"]):
61+
img_path = os.path.join(os.path.dirname(in_file_path), img.attrs["src"])
62+
with open(img_path, "rb") as image_obj:
63+
img_bytes = str(base64.b64encode(image_obj.read()), "utf-8")
64+
img.attrs["src"] = f"data:image/png;base64,{img_bytes}"
65+
Path(out_file_path).write_text(str(soup))
66+
67+
68+
if __name__ == "__main__":
69+
MarkdownIt_obj = MarkdownIt("commonmark").enable("table").enable("strikethrough")
70+
parser = argparse.ArgumentParser()
71+
parser.add_argument(
72+
"--in_file_path",
73+
"-i",
74+
required=True,
75+
help="in-file-path; your existing markdown file",
76+
)
77+
parser.add_argument(
78+
"--out_file_path",
79+
"-o",
80+
default="tmp.html",
81+
help="out-file-path; your HTML file to be created",
82+
)
83+
parser.add_argument(
84+
"--encode_local_images",
85+
"-e",
86+
action="store_true",
87+
help="in HTML, embed base64-encoded data of local images linked to in your markdown; removes dependency on presence of the external local images",
88+
)
89+
parser.add_argument(
90+
"--css_file",
91+
default=None,
92+
help='css-file-path whose content will be written to the <style> element. Can be "none"; do not use any css',
93+
)
94+
args = parser.parse_args()
95+
96+
markdown_to_html(
97+
MarkdownIt_obj=MarkdownIt_obj,
98+
in_file_path=args.in_file_path,
99+
out_file_path=args.out_file_path,
100+
encode_local_images=args.encode_local_images,
101+
css_file=args.css_file,
102+
)

build/lib/httpmdhtml/server.py

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import copy
2+
import datetime
3+
import email.utils
4+
import html
5+
import http.client
6+
import io
7+
import mimetypes
8+
import os
9+
import posixpath
10+
import select
11+
import shutil
12+
import socket # For gethostbyaddr()
13+
import socketserver
14+
import sys
15+
import time
16+
import urllib.parse
17+
import contextlib
18+
import argparse
19+
from functools import partial
20+
from pathlib import Path
21+
22+
from http.server import test as http_server_test # not in http.server.__all__
23+
from http import HTTPStatus
24+
from http.server import *
25+
26+
from markdown_it import MarkdownIt
27+
28+
from bs4 import BeautifulSoup, element
29+
30+
from httpmdhtml import md_to_html
31+
32+
33+
class md_to_html_SimpleHTTPRequestHandler(SimpleHTTPRequestHandler):
34+
def __init__(
35+
self, *args, MarkdownIt_obj=None, css_file=None, live_md_rr=False, **kwargs
36+
):
37+
self.MarkdownIt_obj = MarkdownIt_obj
38+
self.css_file = css_file
39+
self.live_md_rr = live_md_rr
40+
super().__init__(*args, **kwargs)
41+
42+
def do_GET(self, rm_temp_html=False):
43+
"""Serve a GET request."""
44+
self.url_dc_path = urllib.parse.unquote(
45+
urllib.parse.urlsplit(self.path).path
46+
) # url decode, strip query params for file check
47+
if (
48+
self.MarkdownIt_obj
49+
and self.url_dc_path.endswith(".md")
50+
and os.path.exists(os.path.join(self.directory, f".{self.url_dc_path}"))
51+
): # check for markdown file request
52+
in_file_path = os.path.join(self.directory, f".{self.url_dc_path}")
53+
out_file_path = os.path.join(
54+
self.directory, f".{os.path.splitext(self.url_dc_path)[0]}.html"
55+
)
56+
md_to_html.markdown_to_html(
57+
self.MarkdownIt_obj,
58+
in_file_path=in_file_path,
59+
out_file_path=out_file_path,
60+
css_file=self.css_file,
61+
live_md_rr=self.live_md_rr,
62+
)
63+
self.path = f"{os.path.splitext(self.path)[0]}.html"
64+
rm_temp_html = True
65+
f = self.send_head()
66+
if f:
67+
try:
68+
self.copyfile(f, self.wfile)
69+
finally:
70+
f.close()
71+
if rm_temp_html:
72+
os.remove(out_file_path) # remove temp html file
73+
74+
75+
if __name__ == "__main__":
76+
parser = argparse.ArgumentParser()
77+
parser.add_argument("--cgi", action="store_true", help="Run as CGI Server")
78+
parser.add_argument(
79+
"--bind",
80+
"-b",
81+
metavar="ADDRESS",
82+
help="Specify alternate bind address " "[default: all interfaces]",
83+
)
84+
parser.add_argument(
85+
"--directory",
86+
"-d",
87+
default=os.getcwd(),
88+
help="Specify alternative directory " "[default:current directory]",
89+
)
90+
parser.add_argument(
91+
"--css_file",
92+
default=None,
93+
help='css-file-path whose content will be written to the <style> element. Can be "none"; do not use any css',
94+
)
95+
parser.add_argument(
96+
"--live_md_rr",
97+
"-l",
98+
action="store",
99+
type=int,
100+
default=None,
101+
help="Continuous refresh rate of MD page, in ms. Respects cache",
102+
)
103+
parser.add_argument(
104+
"port",
105+
action="store",
106+
default=8000,
107+
type=int,
108+
nargs="?",
109+
help="Specify alternate port [default: 8000]",
110+
)
111+
args = parser.parse_args()
112+
if args.cgi:
113+
handler_class = CGIHTTPRequestHandler
114+
else:
115+
MarkdownIt_obj = (
116+
MarkdownIt("commonmark").enable("table").enable("strikethrough")
117+
)
118+
if args.css_file and all(
119+
(args.css_file != "none", not os.path.isfile(args.css_file))
120+
):
121+
raise FileNotFoundError(
122+
f"looks like the given `css_file` argument's value - {args.css_file} - cannot be found"
123+
)
124+
handler_class = partial(
125+
md_to_html_SimpleHTTPRequestHandler,
126+
directory=args.directory,
127+
MarkdownIt_obj=MarkdownIt_obj,
128+
css_file=args.css_file,
129+
live_md_rr=args.live_md_rr,
130+
)
131+
132+
# ensure dual-stack is not disabled; ref #38907
133+
class DualStackServer(ThreadingHTTPServer):
134+
def server_bind(self):
135+
# suppress exception when protocol is IPv4
136+
with contextlib.suppress(Exception):
137+
self.socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0)
138+
return super().server_bind()
139+
140+
http_server_test(
141+
HandlerClass=handler_class,
142+
ServerClass=DualStackServer,
143+
port=args.port,
144+
bind=args.bind,
145+
)

dark.css

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
body { background-color: #0d1117; color: #e6edf3; font-family: -apple-system,BlinkMacSystemFont,"Segoe UI","Noto Sans",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji"; }
2+
a[href] { color: #66d9ef; }
3+
code { color: #e6edf3; background-color: rgba(110,118,129,0.4); border-radius: 6px; font-family: monospace; white-space: break-spaces; }
4+
table, th, td { border: 1px solid; border-collapse: collapse; padding-left: 4px; padding-right: 4px; }
5+

httpmdhtml/md_to_html.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,16 @@ def markdown_to_html(
3636
else:
3737
with open(css_file, "r") as f:
3838
css = f.read()
39-
else:
40-
css = """body { background-color: #272822; color: white; font-family: Courier; }
41-
a[href] { color: #66d9ef; }
42-
code { color: #ae81ff; background-color: #272b33; border-radius: 6px; }
43-
table, th, td { border: 1px solid; border-collapse: collapse; padding-left: 4px; padding-right: 4px; }"""
39+
else: # #272822
40+
css = """
41+
body { background-color: #272822; color: #e6edf3; font-family: -apple-system,BlinkMacSystemFont,"Segoe UI","Noto Sans",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji"; position: relative; max-width: 960px; margin: auto }
42+
a[href] { color: #66d9ef; }
43+
code { color: #e6edf3; font-family: monospace; white-space: break-spaces; }
44+
p code { padding: .2em .4em; border-radius: 6px; background-color: #343941; }
45+
pre { padding: 1em; border-radius: 6px; background-color: #161b22; }
46+
table, th, td { border: 1px solid; border-collapse: collapse; padding-left: 4px; padding-right: 4px; }
47+
img { max-width: 100%; }
48+
"""
4449
soup.select_one("style").string = css
4550
if live_md_rr:
4651
script = (

setup.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,15 @@
66

77
setup(
88
name="httpmdhtml",
9-
version="0.0.8",
9+
version="0.0.9",
1010
license="gpl-3.0",
1111
author="John Hupperts",
1212
author_email="[email protected]",
1313
description="HTTP server that converts markdown to HTML",
1414
long_description=long_description,
1515
long_description_content_type="text/markdown",
1616
url="https://github.com/treatmesubj/python-md-to-html-server",
17-
download_url="https://github.com/treatmesubj/python-md-to-html-server/archive/refs/tags/v0.0.8.tar.gz",
17+
download_url="https://github.com/treatmesubj/python-md-to-html-server/archive/refs/tags/v0.0.9.tar.gz",
1818
packages=["httpmdhtml"],
1919
package_dir={"python-md-to-html-server": "httpmdhtml"},
2020
project_urls={

0 commit comments

Comments
 (0)