Skip to content

Commit 4586435

Browse files
committed
v0.9.0b1: Partial frontmatter handling
1 parent 8ae73a6 commit 4586435

File tree

6 files changed

+122
-5
lines changed

6 files changed

+122
-5
lines changed

dactyl/common.py

+25
Original file line numberDiff line numberDiff line change
@@ -69,3 +69,28 @@ def guess_title_from_md_file(filepath):
6969
#basically if the first line's not a markdown header, we give up and use
7070
# the filename instead
7171
return os.path.basename(filepath)
72+
73+
def parse_frontmatter(text):
74+
"""Separate YAML frontmatter, if any, from a string, and return the
75+
text separate from the parsed front-matter."""
76+
if len(text) < 6:
77+
logger.debug("...too short for frontmatter")
78+
return text, {}
79+
80+
if text[:3] == "---" and text.find("---", 3) != -1:
81+
logger.debug("...has front matter")
82+
raw_frontmatter = text[3:text.find("---", 3)]
83+
frontmatter = yaml.load(raw_frontmatter)
84+
# Map some Jekyll-specific frontmatter variables to their Dactyl equivs
85+
if "title" in frontmatter.keys():
86+
# We don't care about the Jekyll "page.name" field so it's OK to
87+
# overwrite it.
88+
frontmatter["name"] = frontmatter["title"]
89+
if "categories" in frontmatter.keys() and len(frontmatter["categories"]):
90+
frontmatter["category"] = frontmatter["categories"][0]
91+
print("Loaded frontmatter:", frontmatter)#TODO: remove me
92+
93+
return text[text.find("---", 3)+4:], frontmatter
94+
else:
95+
logger.debug("...no front matter detected")
96+
return text, {}

dactyl/dactyl_build.py

+20-4
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131

3232
from dactyl.config import DactylConfig
3333
from dactyl.cli import DactylCLIParser
34+
from dactyl.jinja_loaders import FrontMatterRemoteLoader, FrontMatterFSLoader
3435

3536
# These fields are special, and pages don't inherit them directly
3637
RESERVED_KEYS_TARGET = [
@@ -296,9 +297,11 @@ def get_categories(pages):
296297
logger.debug("categories: %s" % categories)
297298
return categories
298299

300+
299301
def preprocess_markdown(page, target=None, categories=[], page_filters=[],
300302
mode="html", current_time="TIME_UNKNOWN",
301-
bypass_errors=False, skip_preprocessor="NOT SPECIFIED"):
303+
bypass_errors=False, skip_preprocessor="NOT SPECIFIED",
304+
read_frontmatter=True):
302305
"""Read a markdown file, local or remote, and preprocess it, returning the
303306
preprocessed text."""
304307
target=get_target(target)
@@ -317,13 +320,27 @@ def preprocess_markdown(page, target=None, categories=[], page_filters=[],
317320
with open(page["md"], "r", encoding="utf-8") as f:
318321
md = f.read()
319322

323+
if read_frontmatter:
324+
try:
325+
md, frontmatter = parse_frontmatter(md)
326+
merge_dicts(frontmatter, page)
327+
except Exception as e:
328+
traceback.print_tb(e.__traceback__)
329+
recoverable_error("Error reading frontmatter for page %s: %s" %
330+
(page, repr(e)), bypass_errors)
331+
320332
else:
321333
if config["preprocessor_allow_undefined"] == False and not bypass_errors:
322334
strict_undefined = True
323335
else:
324336
strict_undefined = False
325337
pp_env = setup_pp_env(page, page_filters=page_filters, strict_undefined=strict_undefined)
326338
md_raw = pp_env.get_template(page["md"])
339+
if "fm_map" in dir(pp_env.loader):#TODO: this is a hack
340+
fm_vars = pp_env.loader.fm_map.get(page["md"], {})
341+
else:
342+
fm_vars = {}
343+
merge_dicts(fm_vars, page)
327344
md = md_raw.render(
328345
currentpage=page,
329346
categories=categories,
@@ -334,7 +351,6 @@ def preprocess_markdown(page, target=None, categories=[], page_filters=[],
334351
config=config
335352
)
336353

337-
338354
# Apply markdown-based filters here
339355
for filter_name in page_filters:
340356
if "filter_markdown" in dir(config.filters[filter_name]):
@@ -448,14 +464,14 @@ def setup_pp_env(page=None, page_filters=[], no_loader=False, strict_undefined=F
448464
if remote:
449465
logger.debug("Using remote template loader for page %s" % page)
450466
pp_env = jinja2.Environment(undefined=preferred_undefined,
451-
loader=jinja2.FunctionLoader(read_markdown_remote))
467+
loader=FrontMatterRemoteLoader())
452468
elif no_loader:
453469
logger.debug("Using a no-loader Jinja environment")
454470
pp_env = jinja2.Environment(undefined=preferred_undefined)
455471
else:
456472
logger.debug("Using FileSystemLoader for page %s" % page)
457473
pp_env = jinja2.Environment(undefined=preferred_undefined,
458-
loader=jinja2.FileSystemLoader(path))
474+
loader=FrontMatterFSLoader(path))
459475

460476
# Add custom "defined_and_" tests
461477
def defined_and_equalto(a,b):

dactyl/jinja_loaders.py

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import requests
2+
from jinja2 import BaseLoader, FileSystemLoader, TemplateNotFound, Template
3+
from urllib.parse import urlparse
4+
from dactyl.common import *
5+
6+
class FrontMatterRemoteLoader(BaseLoader):
7+
def __init__(self):
8+
self.baseurl = None
9+
self.fm_map = {} # Save frontmatter so it can be attached to the template
10+
11+
def get_source(self, environment, template):
12+
"""Fetch a remote markdown file and return its contents"""
13+
parsed_url = urlparse(template)
14+
15+
# save this base URL if it's new; that way we can try to let remote
16+
# templates inherit from other templates at related URLs
17+
if parsed_url.scheme or (self.baseurl is None):
18+
base_path = os.path.dirname(parsed_url.path)
19+
self.baseurl = (
20+
parsed_url[0],
21+
parsed_url[1],
22+
base_path,
23+
parsed_url[3],
24+
parsed_url[4],
25+
parsed_url[5],
26+
)
27+
# if we're at read_markdown_remote without a scheme, it's probably a remote
28+
# template trying to import another template, so let's assume it's from the
29+
# base path of the imported template
30+
if not parsed_url.scheme:
31+
url = os.path.join(self.baseurl, template)
32+
else:
33+
url = template
34+
35+
response = requests.get(url)
36+
if response.status_code == 200:
37+
text, frontmatter = parse_frontmatter(response.text)
38+
self.fm_map[template] = frontmatter
39+
return text, url, None
40+
else:
41+
raise TemplateNotFound(template)
42+
43+
class FrontMatterFSLoader(FileSystemLoader):
44+
def __init__(self, searchpath, encoding='utf-8', followlinks=False):
45+
super().__init__(searchpath, encoding, followlinks)
46+
self.fm_map = {}
47+
48+
def get_source(self, environment, template):
49+
text, filename, uptodate = super().get_source(environment, template)
50+
text, frontmatter = parse_frontmatter(text)
51+
self.fm_map[template] = frontmatter
52+
return text, filename, uptodate

dactyl/version.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = '0.8.4'
1+
__version__ = '0.9.0b1'

examples/content/with-frontmatter.md

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
---
2+
desc: This file has Jekyll-style frontmatter
3+
categories: ["Tests", "Dactyl ignores categories beyond the first"]
4+
title: Page With Frontmatter
5+
---
6+
# Page That Has Frontmatter
7+
8+
This page demonstrates a file that contains frontmatter.
9+
10+
The frontmatter can be referenced by the preprocessor. For example:
11+
12+
```
13+
Description: {{"{{"}}currentpage.desc{{"}}"}}
14+
```
15+
16+
Results:
17+
18+
> Description: {{currentpage.desc}}

examples/dactyl-config.yml

+6
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,12 @@ pages:
3434
targets:
3535
- everything
3636

37+
- md: with-frontmatter.md
38+
name: placeholder_with_frontmatter_local
39+
html: with-frontmatter.html
40+
targets:
41+
- everything
42+
3743
- name: Conditionals Test
3844
category: Tests
3945
md: conditionals.md

0 commit comments

Comments
 (0)