Added fenced code with syntax highlighting and auto-escaping braces
This commit is contained in:
parent
cabcda5b00
commit
ae4d30ade9
3
.gitattributes
vendored
3
.gitattributes
vendored
@ -2,4 +2,5 @@
|
||||
* text=auto
|
||||
|
||||
# Ignore Jupyter Notebooks in language stats.
|
||||
*.ipynb linguist-vendored
|
||||
*.ipynb linguist-vendored
|
||||
site/assets/css/codehilite.css linguist-vendored
|
||||
@ -8,18 +8,11 @@ sites:
|
||||
build_cache: ./site
|
||||
assets:
|
||||
- /assets
|
||||
- robots.txt
|
||||
articles:
|
||||
- '*.md'
|
||||
template_selections:
|
||||
article: templates.components.simple_article
|
||||
|
||||
robots_txt:
|
||||
title: robots.txt
|
||||
base_url: http://localhost:8000/robots.txt
|
||||
web_root: ./dist
|
||||
build_cache: ./site
|
||||
assets:
|
||||
- robots.txt
|
||||
|
||||
resume:
|
||||
title: Resume
|
||||
|
||||
@ -5,6 +5,7 @@ import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||
# TODO: add a way to change log level
|
||||
|
||||
from .common import filepath_or_string, GlobalVars, SiteConfig, dotmap_access
|
||||
from .templating import format_html_template, map_templates, TemplateSelections
|
||||
|
||||
@ -2,6 +2,8 @@ import os
|
||||
import glob
|
||||
import yaml
|
||||
import markdown
|
||||
from pygments.formatters import HtmlFormatter
|
||||
from markdown.extensions.codehilite import CodeHiliteExtension
|
||||
import pydantic
|
||||
import logging
|
||||
from typing import Optional, Union
|
||||
@ -13,6 +15,29 @@ from .templating import format_html_template, TemplateSelections
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class CustomHiliteFormatter(HtmlFormatter):
|
||||
'''
|
||||
Override the default Pygments formatter (used by markdown.extensions.codehilite
|
||||
to add syntax highlighting to fenced code) to include the `language-*` class.
|
||||
|
||||
From https://python-markdown.github.io/extensions/code_hilite/
|
||||
'''
|
||||
def __init__(self, lang_str='', **options):
|
||||
super().__init__(**options)
|
||||
self.lang_str = lang_str
|
||||
|
||||
def _wrap_code(self, source):
|
||||
yield 0, f'<code class="{self.lang_str}">'
|
||||
yield from source
|
||||
yield 0, '</code>'
|
||||
|
||||
# Pick extensions to use for Markdown parsing.
|
||||
MARKDOWN_EXTENSIONS = [
|
||||
'footnotes',
|
||||
'fenced_code',
|
||||
CodeHiliteExtension(linenums = True, pygments_formatter = CustomHiliteFormatter)
|
||||
]
|
||||
|
||||
class ArticleMetadata(pydantic.BaseModel):
|
||||
'''A model for the YAML frontmatter included with Markdown articles.'''
|
||||
title: str
|
||||
@ -51,7 +76,7 @@ def load_markdown(md: str) -> tuple[Optional[ArticleMetadata], str]:
|
||||
metadata = yaml.safe_load(raw_metadata)
|
||||
|
||||
# Convert the contents to a HTML string.
|
||||
content = markdown.markdown(raw_article)
|
||||
content = markdown.markdown(raw_article, extensions = MARKDOWN_EXTENSIONS)
|
||||
|
||||
return ArticleMetadata(**metadata), content
|
||||
|
||||
|
||||
@ -58,9 +58,9 @@ def build_blog_archive(
|
||||
|
||||
tag_selector_options = []
|
||||
tag_selector_css_rules = [f'''
|
||||
body:has(input[name="tag-selector"][value="*"]:checked) li:has(.blog-tag){{{{
|
||||
body:has(input[name="tag-selector"][value="*"]:checked) li:has(.blog-tag){{
|
||||
display: list-item!important;
|
||||
}}}}
|
||||
}}
|
||||
''']
|
||||
|
||||
# Add tag selector options in descending order of article count.
|
||||
@ -75,12 +75,12 @@ def build_blog_archive(
|
||||
if tag == '*':
|
||||
continue
|
||||
tag_selector_css_rules.append(f'''
|
||||
body:has(input[name="tag-selector"]:not([value="{tag}"]):checked) li:has(.blog-tag[data="{tag}"]){{{{
|
||||
body:has(input[name="tag-selector"]:not([value="{tag}"]):checked) li:has(.blog-tag[data="{tag}"]){{
|
||||
display: none;
|
||||
}}}}
|
||||
body:has(input[name="tag-selector"][value="{tag}"]:checked) li:has(.blog-tag[data="{tag}"]){{{{
|
||||
}}
|
||||
body:has(input[name="tag-selector"][value="{tag}"]:checked) li:has(.blog-tag[data="{tag}"]){{
|
||||
display: list-item!important;
|
||||
}}}}
|
||||
}}
|
||||
''')
|
||||
|
||||
# For Python 3.9-friendliness.
|
||||
|
||||
@ -1,18 +1,52 @@
|
||||
import os
|
||||
import re
|
||||
|
||||
# Use the community regex package instead of re for QoL
|
||||
# features like variable-length lookbacks.
|
||||
import regex as re
|
||||
import logging
|
||||
from dotmap import DotMap
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
from .common import filepath_or_string, GlobalVars, SiteConfig, dotmap_access
|
||||
|
||||
def escape_braces(template) -> str:
|
||||
'''Escapes any curly braces that are unclosed or enclose
|
||||
anything other than a pattern for a valid variable
|
||||
(alphanumerics, underscores, and periods ONLY;
|
||||
must start with alpha or underscore).
|
||||
'''
|
||||
|
||||
# Match any left braces that are not followed by a valid variable
|
||||
# name (with no whitespace) and a closing right brace.
|
||||
left_brace_pattern = r'{(?![A-Za-z_][\w.]+})'
|
||||
|
||||
# Match any right braces that are not preceded by an opening
|
||||
# left brace and a valid variable name.
|
||||
right_brace_pattern = r'(?<!{[A-Za-z_][\w.]+)}'
|
||||
|
||||
# Escape non-variable-enclosing braces with double braces.
|
||||
return re.sub(
|
||||
right_brace_pattern, '}}',
|
||||
re.sub(
|
||||
left_brace_pattern, '{{', template
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def extract_placeholders(s: str) -> set:
|
||||
'''Extracts placeholder variables in the format `{variable}` from
|
||||
an unformatted template string.'''
|
||||
|
||||
# Regex pattern to match placeholders with alphanumerics, dots, and underscores.
|
||||
placeholder_pattern = r'\{([\w\.]+)\}'
|
||||
placeholder_pattern = r'\{([A-Za-z_][\w.]+)\}'
|
||||
|
||||
# Escape braces that are not used to enclose Python variables
|
||||
# (e.g. those used in inline CSS or JavaScript)
|
||||
escaped = escape_braces(s)
|
||||
|
||||
# Find all matches in the string.
|
||||
matches = re.findall(placeholder_pattern, s)
|
||||
matches = re.findall(placeholder_pattern, escaped)
|
||||
|
||||
# Return the set of distinct placeholders.
|
||||
return set(matches)
|
||||
@ -85,7 +119,19 @@ def format_html_template(template: str, **kwargs) -> str:
|
||||
# there are no more placeholders. The loop is used to account for nested template references.
|
||||
formatted_html = template
|
||||
while len(extract_placeholders(formatted_html)) > 0:
|
||||
formatted_html = formatted_html.format(globalvars = GlobalVars(), **kwargs)
|
||||
|
||||
# Escape non-variable-enclosing braces each iteration of the
|
||||
# loop, because each time they'll be replaced with single braces.
|
||||
try:
|
||||
escaped = escape_braces(formatted_html)
|
||||
formatted_html = escaped.format(
|
||||
globalvars = GlobalVars(),
|
||||
**kwargs
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error('Encountered an exception while formatting the following:')
|
||||
logger.error(escaped)
|
||||
raise e
|
||||
|
||||
# Return the formatted HTML.
|
||||
return formatted_html
|
||||
|
||||
@ -3,4 +3,6 @@ markdown
|
||||
pyyaml
|
||||
rfeed
|
||||
dotmap
|
||||
pydantic
|
||||
pydantic
|
||||
pygments
|
||||
pymdownextensions
|
||||
75
site/assets/css/codehilite.css
vendored
Normal file
75
site/assets/css/codehilite.css
vendored
Normal file
@ -0,0 +1,75 @@
|
||||
pre { line-height: 125%; }
|
||||
td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
|
||||
span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
|
||||
td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
|
||||
span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
|
||||
.codehilite .hll { background-color: #ffffcc }
|
||||
.codehilite { background: #f8f8f8; }
|
||||
.codehilite .c { color: #3D7B7B; font-style: italic } /* Comment */
|
||||
.codehilite .err { border: 1px solid #F00 } /* Error */
|
||||
.codehilite .k { color: #008000; font-weight: bold } /* Keyword */
|
||||
.codehilite .o { color: #666 } /* Operator */
|
||||
.codehilite .ch { color: #3D7B7B; font-style: italic } /* Comment.Hashbang */
|
||||
.codehilite .cm { color: #3D7B7B; font-style: italic } /* Comment.Multiline */
|
||||
.codehilite .cp { color: #9C6500 } /* Comment.Preproc */
|
||||
.codehilite .cpf { color: #3D7B7B; font-style: italic } /* Comment.PreprocFile */
|
||||
.codehilite .c1 { color: #3D7B7B; font-style: italic } /* Comment.Single */
|
||||
.codehilite .cs { color: #3D7B7B; font-style: italic } /* Comment.Special */
|
||||
.codehilite .gd { color: #A00000 } /* Generic.Deleted */
|
||||
.codehilite .ge { font-style: italic } /* Generic.Emph */
|
||||
.codehilite .ges { font-weight: bold; font-style: italic } /* Generic.EmphStrong */
|
||||
.codehilite .gr { color: #E40000 } /* Generic.Error */
|
||||
.codehilite .gh { color: #000080; font-weight: bold } /* Generic.Heading */
|
||||
.codehilite .gi { color: #008400 } /* Generic.Inserted */
|
||||
.codehilite .go { color: #717171 } /* Generic.Output */
|
||||
.codehilite .gp { color: #000080; font-weight: bold } /* Generic.Prompt */
|
||||
.codehilite .gs { font-weight: bold } /* Generic.Strong */
|
||||
.codehilite .gu { color: #800080; font-weight: bold } /* Generic.Subheading */
|
||||
.codehilite .gt { color: #04D } /* Generic.Traceback */
|
||||
.codehilite .kc { color: #008000; font-weight: bold } /* Keyword.Constant */
|
||||
.codehilite .kd { color: #008000; font-weight: bold } /* Keyword.Declaration */
|
||||
.codehilite .kn { color: #008000; font-weight: bold } /* Keyword.Namespace */
|
||||
.codehilite .kp { color: #008000 } /* Keyword.Pseudo */
|
||||
.codehilite .kr { color: #008000; font-weight: bold } /* Keyword.Reserved */
|
||||
.codehilite .kt { color: #B00040 } /* Keyword.Type */
|
||||
.codehilite .m { color: #666 } /* Literal.Number */
|
||||
.codehilite .s { color: #BA2121 } /* Literal.String */
|
||||
.codehilite .na { color: #687822 } /* Name.Attribute */
|
||||
.codehilite .nb { color: #008000 } /* Name.Builtin */
|
||||
.codehilite .nc { color: #00F; font-weight: bold } /* Name.Class */
|
||||
.codehilite .no { color: #800 } /* Name.Constant */
|
||||
.codehilite .nd { color: #A2F } /* Name.Decorator */
|
||||
.codehilite .ni { color: #717171; font-weight: bold } /* Name.Entity */
|
||||
.codehilite .ne { color: #CB3F38; font-weight: bold } /* Name.Exception */
|
||||
.codehilite .nf { color: #00F } /* Name.Function */
|
||||
.codehilite .nl { color: #767600 } /* Name.Label */
|
||||
.codehilite .nn { color: #00F; font-weight: bold } /* Name.Namespace */
|
||||
.codehilite .nt { color: #008000; font-weight: bold } /* Name.Tag */
|
||||
.codehilite .nv { color: #19177C } /* Name.Variable */
|
||||
.codehilite .ow { color: #A2F; font-weight: bold } /* Operator.Word */
|
||||
.codehilite .w { color: #BBB } /* Text.Whitespace */
|
||||
.codehilite .mb { color: #666 } /* Literal.Number.Bin */
|
||||
.codehilite .mf { color: #666 } /* Literal.Number.Float */
|
||||
.codehilite .mh { color: #666 } /* Literal.Number.Hex */
|
||||
.codehilite .mi { color: #666 } /* Literal.Number.Integer */
|
||||
.codehilite .mo { color: #666 } /* Literal.Number.Oct */
|
||||
.codehilite .sa { color: #BA2121 } /* Literal.String.Affix */
|
||||
.codehilite .sb { color: #BA2121 } /* Literal.String.Backtick */
|
||||
.codehilite .sc { color: #BA2121 } /* Literal.String.Char */
|
||||
.codehilite .dl { color: #BA2121 } /* Literal.String.Delimiter */
|
||||
.codehilite .sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */
|
||||
.codehilite .s2 { color: #BA2121 } /* Literal.String.Double */
|
||||
.codehilite .se { color: #AA5D1F; font-weight: bold } /* Literal.String.Escape */
|
||||
.codehilite .sh { color: #BA2121 } /* Literal.String.Heredoc */
|
||||
.codehilite .si { color: #A45A77; font-weight: bold } /* Literal.String.Interpol */
|
||||
.codehilite .sx { color: #008000 } /* Literal.String.Other */
|
||||
.codehilite .sr { color: #A45A77 } /* Literal.String.Regex */
|
||||
.codehilite .s1 { color: #BA2121 } /* Literal.String.Single */
|
||||
.codehilite .ss { color: #19177C } /* Literal.String.Symbol */
|
||||
.codehilite .bp { color: #008000 } /* Name.Builtin.Pseudo */
|
||||
.codehilite .fm { color: #00F } /* Name.Function.Magic */
|
||||
.codehilite .vc { color: #19177C } /* Name.Variable.Class */
|
||||
.codehilite .vg { color: #19177C } /* Name.Variable.Global */
|
||||
.codehilite .vi { color: #19177C } /* Name.Variable.Instance */
|
||||
.codehilite .vm { color: #19177C } /* Name.Variable.Magic */
|
||||
.codehilite .il { color: #666 } /* Literal.Number.Integer.Long */
|
||||
@ -1,3 +1,5 @@
|
||||
/* TODO: Reorganize CSS */
|
||||
|
||||
body{
|
||||
background: inherit;
|
||||
background-color: white;
|
||||
@ -296,4 +298,10 @@ p.socials{
|
||||
|
||||
article ul, article ol{
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
pre:has(> code), mark{
|
||||
font-family: 'Oxygen Mono', sans-serif;
|
||||
font-display: swap;
|
||||
background-color: #d8d8d8;
|
||||
}
|
||||
|
||||
20
site/templates/components/jimoire-article.html
Normal file
20
site/templates/components/jimoire-article.html
Normal file
@ -0,0 +1,20 @@
|
||||
<article>
|
||||
<span class="first-published-label">First published: </span>
|
||||
<time class="first-published-date" pubdate data="{article.metadata.date}" datetime="{article.metadata.date}">{article.metadata.date}</time>
|
||||
<span class="last-modified-label"> · Last modified: </span>
|
||||
<time class="last-modified-date" data="{article.metadata.lastmod}" datetime="{article.metadata.lastmod}">{article.metadata.lastmod}</time>
|
||||
<hr />
|
||||
<h1 class="headline">{article.metadata.title}</h1>
|
||||
<p class="byline">
|
||||
<address class="author">By <a rel="author" href="mailto:admin@jimlab.io">Jim Shepich III</a></address>
|
||||
</p>
|
||||
{article.content}
|
||||
<br /><hr /><br />
|
||||
<p>
|
||||
<!-- TODO: highlightjs.org -->
|
||||
<a href="{site.base_url}/{previous_article.path}" title="{previous_article.metadata.title}" data="{previous_article.path}" class="previous-article-link">< Previous</a>
|
||||
<!-- This non-breaking space prevents the RSS icon from getting weird when previous-article-link is hidden. -->
|
||||
<a href="{site.base_url}/{next_article.path}" title="{next_article.metadata.title}" data="{next_article.path}" class="next-article-link">Next ></a>
|
||||
</p>
|
||||
<p>Tags: {blog_tags}{templates.components.rss_icon}</p>
|
||||
</article>
|
||||
@ -1,5 +1,6 @@
|
||||
<link rel="stylesheet" type="text/css" href="/assets/css/reset.css">
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Jacquard+12|Fira+Sans|UnifrakturMaguntia">
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Jacquard+12|Fira+Sans|UnifrakturMaguntia|Oxygen+Mono">
|
||||
<link rel="stylesheet" type="text/css" href="/assets/css/common.css">
|
||||
<link rel="stylesheet" type="text/css" href="/assets/css/layout.css">
|
||||
<link rel="stylesheet" type="text/css" href="/assets/css/theme.css">
|
||||
<link rel="stylesheet" type="text/css" href="/assets/css/codehilite.css">
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user