Added article on how to build a drone from scratch
|
@ -8,6 +8,8 @@ from typing import Optional
|
|||
from flask import Flask, Response, abort, send_from_directory, render_template
|
||||
from markdown import markdown
|
||||
|
||||
from .latex import MarkdownLatex
|
||||
|
||||
basedir = os.path.abspath(os.path.join(os.path.realpath(__file__), '..', '..'))
|
||||
templates_dir = os.path.join(basedir, 'templates')
|
||||
static_dir = os.path.join(basedir, 'static')
|
||||
|
@ -66,7 +68,7 @@ def get_page(page: str, title: Optional[str] = None, skip_header: bool = False):
|
|||
author_email=re.match(r'(.+?)\s+<([^>]+)>', metadata['author'])[2] if 'author' in metadata else None,
|
||||
published=(metadata['published'].strftime('%b %d, %Y')
|
||||
if metadata.get('published') else None),
|
||||
content=markdown(f.read(), extensions=['fenced_code', 'codehilite']),
|
||||
content=markdown(f.read(), extensions=['fenced_code', 'codehilite', MarkdownLatex()]),
|
||||
skip_header=skip_header)
|
||||
|
||||
|
||||
|
|
243
app/latex.py
Normal file
|
@ -0,0 +1,243 @@
|
|||
"""
|
||||
Licensed under Public Domain Mark 1.0.
|
||||
See https://creativecommons.org/publicdomain/mark/1.0/
|
||||
Author: Justin Bruce Van Horne <justinvh@gmail.com>
|
||||
|
||||
Python-Markdown LaTeX Extension
|
||||
Adds support for $math mode$ and %text mode%. This plugin supports
|
||||
multiline equations/text.
|
||||
The actual image generation is done via LaTeX/DVI output.
|
||||
It encodes data as base64 so there is no need for images directly.
|
||||
All the work is done in the preprocessor.
|
||||
"""
|
||||
|
||||
import base64
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import tempfile
|
||||
from subprocess import call as rawcall, PIPE
|
||||
|
||||
import markdown
|
||||
|
||||
|
||||
def call(*args, **kwargs):
|
||||
"""
|
||||
Proxy to subprocess.call(), removes timeout argument in case of
|
||||
Python2 because that was only implemented in Python3.
|
||||
"""
|
||||
return rawcall(*args, **kwargs)
|
||||
|
||||
|
||||
# Defines our basic inline image
|
||||
img_expr = '<img class="latex inline math-%s" alt="%s" id="%s" src="data:image/png;base64,%s">'
|
||||
|
||||
# Defines multiline expression image
|
||||
multiline_img_expr = '''<div class="multiline-wrapper">
|
||||
<img class="latex multiline math-%s" alt="%s" id="%s" src="data:image/png;base64,%s"></div>'''
|
||||
|
||||
# Base CSS template
|
||||
img_css = """<style scoped>
|
||||
.multiline-wrapper {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
img.latex.multiline {
|
||||
height: 65%;
|
||||
}
|
||||
|
||||
img.latex.inline {
|
||||
height: .9em;
|
||||
vertical-align: middle;
|
||||
}
|
||||
</style>"""
|
||||
|
||||
# Cache and temp file paths
|
||||
tmpdir = tempfile.gettempdir() + '/markdown-latex'
|
||||
cache_file = tmpdir + '/latex.cache'
|
||||
|
||||
|
||||
class LaTeXPreprocessor(markdown.preprocessors.Preprocessor):
|
||||
# These are our cached expressions that are stored in latex.cache
|
||||
cached = {}
|
||||
|
||||
# Basic LaTex Setup as well as our list of expressions to parse
|
||||
tex_preamble = r"""\documentclass[14pt]{article}
|
||||
\usepackage{amsmath}
|
||||
\usepackage{amsthm}
|
||||
\usepackage{amssymb}
|
||||
\usepackage{bm}
|
||||
\usepackage{graphicx}
|
||||
\usepackage[usenames,dvipsnames]{color}
|
||||
\pagestyle{empty}
|
||||
"""
|
||||
|
||||
# Math TeX extraction regex
|
||||
math_extract_regex = re.compile(r'(.+?)((\\\(.+?\\\))|(\$\$\n.+?\n\$\$\n))(.*)', re.MULTILINE | re.DOTALL)
|
||||
|
||||
# Math TeX matching regex
|
||||
math_match_regex = re.compile(r'\s*(\\\(.+?\\\))|(\$\$\n.+?\n\$\$\n)\s*', re.MULTILINE | re.DOTALL)
|
||||
|
||||
def __init__(self, *_, **__):
|
||||
if not os.path.isdir(tmpdir):
|
||||
os.makedirs(tmpdir)
|
||||
try:
|
||||
with open(cache_file, 'r') as f:
|
||||
self.cached = json.load(f)
|
||||
except (IOError, json.JSONDecodeError):
|
||||
self.cached = {}
|
||||
|
||||
self.config = {
|
||||
("general", "preamble"): "",
|
||||
("dvipng", "args"): "-q -T tight -bg Transparent -z 9 -D 200",
|
||||
("delimiters", "text"): "%",
|
||||
("delimiters", "math"): "$",
|
||||
("delimiters", "preamble"): "%%"}
|
||||
|
||||
def _latex_to_base64(self, tex):
|
||||
"""Generates a base64 representation of TeX string"""
|
||||
|
||||
# Generate the temporary file
|
||||
tmp_file_fd, path = tempfile.mkstemp(dir=tmpdir)
|
||||
with os.fdopen(tmp_file_fd, "w") as tmp_file:
|
||||
tmp_file.write(self.tex_preamble)
|
||||
tmp_file.write(tex)
|
||||
tmp_file.write('\n\\end{document}')
|
||||
|
||||
# compile LaTeX document. A DVI file is created
|
||||
status = call(('latex -halt-on-error -output-directory={:s} {:s}'
|
||||
.format(tmpdir, path)).split(),
|
||||
stdout=PIPE, timeout=10)
|
||||
|
||||
# clean up if the above failed
|
||||
if status:
|
||||
self._cleanup(path, err=True)
|
||||
raise Exception("Couldn't compile LaTeX document." +
|
||||
"Please read '%s.log' for more detail." % path)
|
||||
|
||||
# Run dvipng on the generated DVI file. Use tight bounding box.
|
||||
# Magnification is set to 1200
|
||||
dvi = "%s.dvi" % path
|
||||
png = "%s.png" % path
|
||||
|
||||
# Extract the image
|
||||
cmd = "dvipng %s %s -o %s" % (self.config[("dvipng", "args")], dvi, png)
|
||||
status = call(cmd.split(), stdout=PIPE)
|
||||
|
||||
# clean up if we couldn't make the above work
|
||||
if status:
|
||||
self._cleanup(path, err=True)
|
||||
raise Exception("Couldn't convert LaTeX to image." +
|
||||
"Please read '%s.log' for more detail." % path)
|
||||
|
||||
# Read the png and encode the data
|
||||
try:
|
||||
with open(png, "rb") as png:
|
||||
data = png.read()
|
||||
return base64.b64encode(data)
|
||||
finally:
|
||||
self._cleanup(path)
|
||||
|
||||
@staticmethod
|
||||
def _cleanup(path, err=False):
|
||||
# don't clean up the log if there's an error
|
||||
extensions = ["", ".aux", ".dvi", ".png", ".log"]
|
||||
if err:
|
||||
extensions.pop()
|
||||
|
||||
# now do the actual cleanup, passing on non-existent files
|
||||
for extension in extensions:
|
||||
try:
|
||||
os.remove("%s%s" % (path, extension))
|
||||
except (IOError, OSError):
|
||||
pass
|
||||
|
||||
def run(self, lines):
|
||||
"""Parses the actual page"""
|
||||
# Re-creates the entire page so we can parse in a multine env.
|
||||
page = "\n".join(lines)
|
||||
|
||||
# Adds a preamble mode
|
||||
self.tex_preamble += self.config[("general", "preamble")] + "\n\\begin{document}\n"
|
||||
|
||||
# Figure out our text strings and math-mode strings
|
||||
tex_expr = self.math_extract_regex.findall(page)
|
||||
|
||||
# No sense in doing the extra work
|
||||
if not len(tex_expr):
|
||||
return page.split("\n")
|
||||
|
||||
# Parse the expressions
|
||||
new_cache = {}
|
||||
new_page = ''
|
||||
n_multiline_expressions = 0
|
||||
|
||||
while page:
|
||||
m = self.math_extract_regex.match(page)
|
||||
if not m:
|
||||
new_page += page
|
||||
break
|
||||
|
||||
new_page += m.group(1)
|
||||
math_match = self.math_match_regex.match(m.group(2))
|
||||
if not math_match:
|
||||
new_page += m.group(2)
|
||||
else:
|
||||
expr = m.group(2)
|
||||
is_multiline = math_match.group(2) is not None
|
||||
tex_hash = self.hash(expr)
|
||||
if tex_hash in self.cached:
|
||||
data = self.cached[tex_hash]
|
||||
else:
|
||||
data = self._latex_to_base64(expr).decode()
|
||||
new_cache[tex_hash] = data
|
||||
|
||||
if is_multiline and n_multiline_expressions > 0:
|
||||
new_page += '</p>'
|
||||
new_page += (multiline_img_expr if is_multiline else img_expr) % ('true', expr, tex_hash, data)
|
||||
|
||||
if is_multiline:
|
||||
new_page += '<p>'
|
||||
n_multiline_expressions += 1
|
||||
|
||||
page = m.group(5)
|
||||
|
||||
if n_multiline_expressions > 0:
|
||||
new_page += '</p>'
|
||||
|
||||
# Cache our data
|
||||
self.cached.update(new_cache)
|
||||
with open(cache_file, 'w') as f:
|
||||
json.dump(self.cached, f)
|
||||
|
||||
# Make sure to re-split the lines
|
||||
return new_page.split("\n")
|
||||
|
||||
@staticmethod
|
||||
def hash(tex: str) -> str:
|
||||
return hashlib.sha1(tex.encode()).hexdigest()
|
||||
|
||||
|
||||
class LaTeXPostprocessor(markdown.postprocessors.Postprocessor):
|
||||
"""This post processor extension just allows us to further
|
||||
refine, if necessary, the document after it has been parsed."""
|
||||
|
||||
# noinspection PyMethodMayBeStatic
|
||||
def run(self, text):
|
||||
# Inline a style for default behavior
|
||||
text = img_css + text
|
||||
return text
|
||||
|
||||
|
||||
class MarkdownLatex(markdown.Extension):
|
||||
"""Wrapper for LaTeXPreprocessor"""
|
||||
|
||||
def extendMarkdown(self, md):
|
||||
# Our base LaTeX extension
|
||||
md.preprocessors.add('latex',
|
||||
LaTeXPreprocessor(self), ">html_block")
|
||||
# Our cleanup postprocessing extension
|
||||
md.postprocessors.add('latex',
|
||||
LaTeXPostprocessor(self), ">amp_substitute")
|
BIN
static/img/adafruit-16-pwm.jpg
Normal file
After Width: | Height: | Size: 74 KiB |
BIN
static/img/brushless-motor-1.png
Normal file
After Width: | Height: | Size: 86 KiB |
BIN
static/img/drone-lift.gif
Normal file
After Width: | Height: | Size: 8.7 KiB |
BIN
static/img/drone-schema.png
Normal file
After Width: | Height: | Size: 60 KiB |
BIN
static/img/drone-warning.png
Normal file
After Width: | Height: | Size: 161 KiB |
BIN
static/img/esc-1.jpg
Normal file
After Width: | Height: | Size: 26 KiB |
BIN
static/img/esc-pwm.png
Normal file
After Width: | Height: | Size: 49 KiB |
BIN
static/img/lipo-1.jpg
Normal file
After Width: | Height: | Size: 57 KiB |
BIN
static/img/pitch-roll-yaw-1.png
Normal file
After Width: | Height: | Size: 84 KiB |
BIN
static/img/pitch-roll-yaw-2.png
Normal file
After Width: | Height: | Size: 23 KiB |
BIN
static/img/propeller-1.png
Normal file
After Width: | Height: | Size: 21 KiB |
BIN
static/img/propeller-schema.png
Normal file
After Width: | Height: | Size: 221 KiB |
BIN
static/img/pwm-1.png
Normal file
After Width: | Height: | Size: 7.5 KiB |
BIN
static/img/rpi-pinout.jpg
Normal file
After Width: | Height: | Size: 109 KiB |
BIN
static/img/static-thrust-1.png
Normal file
After Width: | Height: | Size: 57 KiB |
BIN
static/img/xt60-board.jpg
Normal file
After Width: | Height: | Size: 72 KiB |