Skip to content

Commit c018e58

Browse files
committed
Merge branch 'frontmatter' into v0.9.0
2 parents ee9e25f + 4586435 commit c018e58

File tree

6 files changed

+124
-5
lines changed

6 files changed

+124
-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

+22-4
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
from dactyl.config import DactylConfig
3636
from dactyl.cli import DactylCLIParser
3737
from dactyl.openapi import ApiDef
38+
from dactyl.jinja_loaders import FrontMatterRemoteLoader, FrontMatterFSLoader
3839

3940
# These fields are special, and pages don't inherit them directly
4041
RESERVED_KEYS_TARGET = [
@@ -360,9 +361,11 @@ def get_categories(pages):
360361
logger.debug("categories: %s" % categories)
361362
return categories
362363

364+
363365
def preprocess_markdown(page, target=None, categories=[], page_filters=[],
364366
mode="html", current_time="TIME_UNKNOWN",
365-
bypass_errors=False, skip_preprocessor="NOT SPECIFIED"):
367+
bypass_errors=False, skip_preprocessor="NOT SPECIFIED",
368+
read_frontmatter=True):
366369
"""Read a markdown file, local or remote, and preprocess it, returning the
367370
preprocessed text."""
368371
target=get_target(target)
@@ -384,6 +387,15 @@ def preprocess_markdown(page, target=None, categories=[], page_filters=[],
384387
with open(page["md"], "r", encoding="utf-8") as f:
385388
md = f.read()
386389

390+
if read_frontmatter:
391+
try:
392+
md, frontmatter = parse_frontmatter(md)
393+
merge_dicts(frontmatter, page)
394+
except Exception as e:
395+
traceback.print_tb(e.__traceback__)
396+
recoverable_error("Error reading frontmatter for page %s: %s" %
397+
(page, repr(e)), bypass_errors)
398+
387399
else:
388400
if config["preprocessor_allow_undefined"] == False and not bypass_errors:
389401
strict_undefined = True
@@ -394,6 +406,13 @@ def preprocess_markdown(page, target=None, categories=[], page_filters=[],
394406
md_raw = pp_env.get_template("_")
395407
else:
396408
md_raw = pp_env.get_template(page["md"])
409+
410+
if "fm_map" in dir(pp_env.loader): #TODO: this is a hack
411+
fm_vars = pp_env.loader.fm_map.get(page["md"], {})
412+
else:
413+
fm_vars = {}
414+
merge_dicts(fm_vars, page)
415+
397416
md = md_raw.render(
398417
currentpage=page,
399418
categories=categories,
@@ -404,7 +423,6 @@ def preprocess_markdown(page, target=None, categories=[], page_filters=[],
404423
config=config
405424
)
406425

407-
408426
# Apply markdown-based filters here
409427
for filter_name in page_filters:
410428
if "filter_markdown" in dir(config.filters[filter_name]):
@@ -525,7 +543,7 @@ def setup_pp_env(page=None, page_filters=[], no_loader=False, strict_undefined=F
525543
if how == HOW_FROM_URL:
526544
logger.debug("Using remote template loader for page %s" % page)
527545
pp_env = jinja2.Environment(undefined=preferred_undefined,
528-
loader=jinja2.FunctionLoader(read_markdown_remote))
546+
loader=FrontMatterRemoteLoader())
529547
elif how == HOW_FROM_GENERATOR:
530548
logger.debug("Using a generator-loader for page %s" % page)
531549
pp_env = jinja2.Environment(undefined=preferred_undefined,
@@ -536,7 +554,7 @@ def setup_pp_env(page=None, page_filters=[], no_loader=False, strict_undefined=F
536554
else:
537555
logger.debug("Using FileSystemLoader for page %s" % page)
538556
pp_env = jinja2.Environment(undefined=preferred_undefined,
539-
loader=jinja2.FileSystemLoader(path))
557+
loader=FrontMatterFSLoader(path))
540558

541559
# Add custom "defined_and_" tests
542560
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.9.0ascanius94'
1+
__version__ = '0.9.0b2'

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)