Skip to content

Commit

Permalink
v0.13.2: hover anchors, filter+frontmatter bugfixes
Browse files Browse the repository at this point in the history
  • Loading branch information
mDuo13 committed Jan 8, 2021
1 parent 7f128fa commit 158e914
Show file tree
Hide file tree
Showing 8 changed files with 177 additions and 54 deletions.
3 changes: 2 additions & 1 deletion dactyl/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
NOT_LOADED_PLACEHOLDER = "__NOT_LOADED_PLACEHOLDER__"
API_SLUG_KEY = "api_slug"
BUILTIN_ES_TEMPLATE = "templates/template-es.json"
HOVERANCHOR_FIELD = "hover_anchors"

DEFAULT_SERVER_PORT = 32289 # "DACTY" in T-9

Expand Down Expand Up @@ -111,7 +112,7 @@ def merge_dicts(default_d, specific_d, reserved_keys_top=[], override=False):
if key not in specific_d.keys():
specific_d[key] = val
elif type(specific_d[key]) == dict and type(val) == dict:
merge_dicts(val, specific_d[key])
merge_dicts(val, specific_d[key])
elif override:
specific_d[key] = val
#else leave the key in the specific_d
81 changes: 46 additions & 35 deletions dactyl/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,43 +144,54 @@ def load_filters(self):

# Try loading from custom filter paths in order, fall back to built-ins
for filter_name in filternames:
filter_loaded = False
loading_errors = []
if "filter_paths" in self.config:
for filter_path in self.config["filter_paths"]:
try:
f_filepath = os.path.join(filter_path, "filter_"+filter_name+".py")

## Requires Python 3.5+
spec = importlib.util.spec_from_file_location(
"dactyl_filters."+filter_name, f_filepath)
self.filters[filter_name] = importlib.util.module_from_spec(spec)
spec.loader.exec_module(self.filters[filter_name])

filter_loaded = True
break
except FileNotFoundError as e:
loading_errors.append({"Path": filter_path, "Error": repr(e)})
logger.debug("Filter %s isn't in path %s\nErr:%s" %
(filter_name, filter_path, repr(e)))
except Exception as e:
loading_errors.append({"Path": filter_path, "Error": repr(e)})
recoverable_error("Failed to load filter '%s', with error: %s" %
(filter_name, repr(e)), self.bypass_errors)

if not filter_loaded:
# Load from the Dactyl module
self.load_filter(filter_name)

def load_filter(self, filter_name):
"""
Load a specific filter, if possible. Can be called "late" when parsing
frontmatter.
"""
if filter_name in self.filters.keys():
return True

loading_errors = []
if "filter_paths" in self.config:
for filter_path in self.config["filter_paths"]:
try:
self.filters[filter_name] = import_module("dactyl.filter_"+filter_name)
f_filepath = os.path.join(filter_path, "filter_"+filter_name+".py")

## Requires Python 3.5+
spec = importlib.util.spec_from_file_location(
"dactyl_filters."+filter_name, f_filepath)
self.filters[filter_name] = importlib.util.module_from_spec(spec)
spec.loader.exec_module(self.filters[filter_name])

return True

except FileNotFoundError as e:
loading_errors.append({"Path": filter_path, "Error": repr(e)})
logger.debug("Filter %s isn't in path %s\nErr:%s" %
(filter_name, filter_path, repr(e)))
except Exception as e:
loading_errors.append({"Path": "(Dactyl Built-ins)", "Error": repr(e)})
#logger.debug("Failed to load filter %s. Errors: %s" %
# (filter_name, loading_errors))
recoverable_error("Failed to load filter %s. Errors:\n%s" %
(filter_name, "\n".join(
[" %s: %s" % (le["Path"], le["Error"])
for le in loading_errors])
), self.bypass_errors)
loading_errors.append({"Path": filter_path, "Error": repr(e)})
recoverable_error("Failed to load filter '%s', with error: %s" %
(filter_name, repr(e)), self.bypass_errors)

# Didn't find it yet; try loading it from the Dactyl module
try:
self.filters[filter_name] = import_module("dactyl.filter_"+filter_name)
return True
except Exception as e:
loading_errors.append({"Path": "(Dactyl Built-ins)", "Error": repr(e)})
#logger.debug("Failed to load filter %s. Errors: %s" %
# (filter_name, loading_errors))
recoverable_error("Failed to load filter %s. Errors:\n%s" %
(filter_name, "\n".join(
[" %s: %s" % (le["Path"], le["Error"])
for le in loading_errors])
), self.bypass_errors)



def load_build_options(self):
"""Overwrites some build-specific options based on the CLI params"""
Expand Down
60 changes: 44 additions & 16 deletions dactyl/page.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ def __init__(self, config, data, skip_pp=False, more_filters=[]):
self.load_content()
self.provide_default_filename()
self.provide_name()
# self.html_content({"currentpage":data, **context})

def get_pp_env(self, loader):
if (self.config["preprocessor_allow_undefined"] or
Expand All @@ -53,17 +52,6 @@ def undefined_or_ne(a,b):
return pp_env.tests["undefined"](a) or pp_env.tests["ne"](a, b)
pp_env.tests["undefined_or_ne"] = undefined_or_ne

# Pull exported values (& functions) from page filters into the pp_env
for filter_name in self.filters(save=False):
if filter_name not in self.config.filters.keys():
logger.debug("Skipping unloaded filter '%s'" % filter_name)
continue
if "export" in dir(self.config.filters[filter_name]):
for key,val in self.config.filters[filter_name].export.items():
logger.debug("... pulling in filter_%s's exported key '%s'"
% (filter_name, key))
pp_env.globals[key] = val

return pp_env

def load_content(self):
Expand Down Expand Up @@ -127,6 +115,9 @@ def load_from_disk(self):
# special case: let frontmatter overwrite default "html" vals
if PROVIDED_FILENAME_KEY in self.data and "html" in frontmatter:
self.data["html"] = frontmatter["html"]
# special case: add filters from frontmatter
if "filters" in frontmatter:
self.gain_filters(frontmatter["filters"])
self.twolines = pp_env.loader.twolines[self.data["md"]]
else:
logger.info("... reading markdown from file")
Expand All @@ -138,6 +129,8 @@ def load_from_disk(self):
# special case: let frontmatter overwrite default "html" vals
if PROVIDED_FILENAME_KEY in self.data and "html" in frontmatter:
self.data["html"] = frontmatter["html"]
if "filters" in frontmatter:
self.gain_filters(frontmatter["filters"])
self.twolines = self.rawtext.split("\n", 2)[:2]


Expand Down Expand Up @@ -219,7 +212,7 @@ def provide_name(self):

def preprocess(self, context):
try:
md = self.pp_template.render(**context)
md = self.pp_template.render(**context, **self.filter_exports())
except Exception as e:
recoverable_error("Preprocessor error in page %s: %s."%(self, e),
self.config.bypass_errors, error=e)
Expand Down Expand Up @@ -345,8 +338,9 @@ def idify(utext):

def update_toc(self):
"""
Assign unique IDs to header elements in a BeautifulSoup object, and
update internal table of contents accordingly.
Assign unique IDs to header elements in the BeautifulSoup object, and
update internal table of contents accordingly. Also add "hover anchors"
to headers in the BeautifulSoup object.
The resulting ToC is a list of objects, each in the form:
{
"text": "Header Content as Text",
Expand All @@ -363,6 +357,7 @@ def update_toc(self):
uniqIDs = {}
headermap = {}
headers = self.soup.find_all(name=re.compile("h[1-6]"))
hoveranchor_contents = self.data.get("hover_anchors", False)
for h in headers:
h_id = self.idify(h.get_text())
if h_id not in uniqIDs.keys():
Expand All @@ -378,6 +373,17 @@ def update_toc(self):
"id": h_id,
"level": int(h.name[1])
})

if hoveranchor_contents:
hoverlink = self.soup.new_tag("a", attrs={
"href": "#"+h_id,
"class": "hover_anchor",
"aria-hidden": "true"})
# parse & insert the configured text/HTML contents for the anchors
hoverlink.append(BeautifulSoup(hoveranchor_contents, "html.parser"))
h.append(hoverlink)


# ElasticSearch doesn't like dots in keys, so escape those
escaped_name = h.get_text().replace(".","-")
headermap[escaped_name] = "#"+h_id
Expand Down Expand Up @@ -434,6 +440,7 @@ def render(self, use_template, context):
page_toc=self.legacy_toc(),
headers=self.toc,
**context,
**self.filter_exports(),
)
return out_html

Expand Down Expand Up @@ -520,12 +527,33 @@ def filepath(self, mode):

def gain_filters(self, filterlist):
"""
Called by target to add its filters to this page's.
Add the target's filters to this page's list.
"""
if "filters" in self.data:
self.data["filters"] = filterlist + self.data["filters"]
else:
self.data["filters"] = filterlist
for filter_name in filterlist:
self.config.load_filter(filter_name)

def filter_exports(self):
"""
Return a set of values exported by filters to be added to the context
when preprocessing and rending the page.
"""

exported_vals = {}
page_filters = self.filters(save=False)
for filter_name in page_filters:
if filter_name not in self.config.filters.keys():
logger.debug("Skipping unloaded filter '%s'" % filter_name)
continue
if "export" in dir(self.config.filters[filter_name]):
for key,val in self.config.filters[filter_name].export.items():
if key in exported_vals.keys():
logger.warning(f"Export '{key}' from filter {filter_name} overwrites previous value. Another filter exported the same key?")
exported_vals[key] = val
return exported_vals

def filters(self, save=True):
"""
Expand Down
15 changes: 15 additions & 0 deletions dactyl/styles/dactyl.scss
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,21 @@ $navbar-nav-link-padding-x: 1rem;
font-weight: 700;
}

// Hover anchors ---------------
.hover_anchor {
visibility: hidden;
padding-left: 1rem;
font-size: 60%;
}

h1,h2,h3,h4,h5,h6 {
&:hover .hover_anchor {
visibility: visible;
text-decoration: none;
}
}


// Images --------------------------------------------------------------------

// Images should not exceed the main column
Expand Down
2 changes: 1 addition & 1 deletion dactyl/version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = '0.13.1'
__version__ = '0.13.2'
6 changes: 6 additions & 0 deletions examples/dactyl-config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ filter_paths:
- custom_filters
- more_custom_filters

defaults: &defaults
hover_anchors: ""

targets:
- name: everything
display_name: Dactyl Examples
Expand All @@ -13,13 +16,16 @@ targets:
demote_headers_pdf_only: true
stylesheet: template_assets/dactyl.css
foo: "fooooooo"
<<: *defaults

- name: filterdemos
display_name: Target with just the filter example pages
<<: *defaults

- name: conditionals
display_name: Conditional Text Target
condition: tests-2
<<: *defaults

pages:
- name: Tests
Expand Down
2 changes: 1 addition & 1 deletion examples/template_assets/dactyl.css

Large diffs are not rendered by default.

62 changes: 62 additions & 0 deletions releasenotes.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,65 @@
# v0.13.2 Release Notes

This release adds "hover anchors" and fixes two bugs with filters and frontmatter.

## Bug Fixes

- Fixed a problem with the `export` values of filters not being loaded when those filters were enabled via frontmatter on some pages.
- Fixed a problem where filters would not get imported if they were only listed in frontmatter sections and not in the main config file somewhere.

### Hover Anchors

Added the option to enable "hover anchors" for linking directly to headers. These are little links that appear next to a header so that you can right-click and copy the link to get a URL directly to that header. To enable hover anchors, you need to make two changes:

1. In your Dactyl config file, add a `hover_anchors` field to the definition of the target(s) or page(s) you want to enable them on. Set the value to whatever text or HTML you want to represent the anchor. Some examples:

Plain text octothorpe:

- name: some_target
hover_anchors: "#"

FontAwesome 4 link icon:

- name: some_target
hover_anchors: <i class="fa fa-link"></i>

2. In your stylesheet, add styles to show `.hover_anchor` elements only when headers are hovered. For example:

CSS:

.hover_anchor {
visibility: hidden;
padding-left: 1rem;
font-size: 60%;
text-decoration: none;
}

h1:hover .hover_anchor, h2:hover .hover_anchor,
h3:hover .hover_anchor, h4:hover .hover_anchor,
h5:hover .hover_anchor, h6:hover .hover_anchor {
visibility: visible;
}

Or, SCSS, if you prefer:

.dactyl_content {
// Hover anchors ---------------
.hover_anchor {
visibility: hidden;
padding-left: 1rem;
font-size: 60%;
}

h1,h2,h3,h4,h5,h6 {
&:hover .hover_anchor {
visibility: visible;
text-decoration: none;
}
}
}



# v0.13.1 Release Notes

This release fixes the link checker's handling of some less common hyperlink types. It also adds the `--legacy_prince` option to allow you to build PDFs with Prince version 10 and earlier.
Expand Down

0 comments on commit 158e914

Please sign in to comment.