Skip to content

Commit e1c96bb

Browse files
committed
v0.3.0: workaround Prince filepath bug; multiple content_static_path; regex image subs; pass in custom vars
1 parent 3c04d30 commit e1c96bb

File tree

4 files changed

+155
-35
lines changed

4 files changed

+155
-35
lines changed

README.md

+43-5
Original file line numberDiff line numberDiff line change
@@ -184,11 +184,12 @@ targets:
184184

185185
In addition to `name` and `display_name`, a target definition can contain the following fields:
186186

187-
| Field | Type | Description |
188-
|:-------------|:-----------|:-------------------------------------------------|
189-
| `filters` | Array | Names of filters to apply to all pages in this target. |
190-
| `image_subs` | Dictionary | Mapping of image paths to replacement paths that should be used when building this target. (Use this, for example, to provide absolute paths to images uploaded to a CDN or CMS.) |
191-
| ... | (Various) | Arbitrary key-values to be inherited by all pages in this target. (You can use this for pre-processing or in templates.) The following field names cannot be used: `name`, `display_name`, `image_subs`, `filters`, and `pages`. |
187+
| Field | Type | Description |
188+
|:----------------|:-----------|:----------------------------------------------|
189+
| `filters` | Array | Names of filters to apply to all pages in this target. |
190+
| `image_subs` | Dictionary | Mapping of image paths to replacement paths that should be used when building this target. (Use this, for example, to provide absolute paths to images uploaded to a CDN or CMS.) |
191+
| `image_re_subs` | Dictionary | Same as `image_subs`, but the keys are regular expressions and the patterns can contain backreferences to matched subgroups (e.g. `\\1` for the first parenthetical group). |
192+
| ... | (Various) | Arbitrary key-values to be inherited by all pages in this target. (You can use this for pre-processing or in templates.) The following field names cannot be used: `name`, `display_name`, `image_subs`, `filters`, and `pages`. |
192193

193194
### Pages
194195

@@ -250,6 +251,43 @@ Dactyl pre-processes Markdown files by treating them as [Jinja][] Templates, so
250251
| `pages` | The [array of page definitions](#pages) in the current target. |
251252
| `currentpage` | The definition of the page currently being rendered. |
252253

254+
255+
### Adding Variables from the Commandline
256+
257+
You can pass in a JSON or YAML-formatted list of variables using `--vars` commandline switch. Any such variables get added as fields of `target` and inherited by `currentpage` in any case where `currentpage` does not already have the same variable name set. For example:
258+
259+
```sh
260+
$ cat md/vartest.md
261+
Myvar is: '{{ target.myvar }}'
262+
263+
$ dactyl_build --vars '{"myvar":"foo"}'
264+
rendering pages...
265+
writing to file: out/index.html...
266+
Preparing page vartest.md
267+
reading markdown from file: vartest.md
268+
... parsing markdown...
269+
... modifying links for target: default
270+
... re-rendering HTML from soup...
271+
writing to file: out/test_vars.html...
272+
done rendering
273+
copying static pages...
274+
275+
$ cat out/test_vars.html | grep Myvar
276+
<p>Myvar is: 'foo'</p></main>
277+
```
278+
279+
If argument to `--vars` ends in `.yaml` or `.json`, Dactyl treats the argument as a filename and opens it as a YAML file. (YAML is a superset of JSON, so this works for JSON files.) Otherwise, Dactyl treats the argument as a YAML/JSON object directly. Be sure that the argument is quoted and escaped as necessary based on the commandline shell you use.
280+
281+
You cannot set the following reserved keys:
282+
283+
- `name`
284+
- `display_name` (Instead, use the `--title` argument to set the display name of the target on the commandline.)
285+
- `filters`
286+
- `image_subs`
287+
- `image_re_subs`
288+
- `pages`
289+
290+
253291
### Filters
254292

255293
Furthermore, Dactyl supports additional custom post-processing through the use of filters. Filters can operate on the markdown (after it's been pre-processed), on the raw HTML (after it's been parsed), or on a BeautifulSoup object representing the output HTML. Dactyl comes with several filters, which you can enable in your config file. Support for user-defined filters is planned but not yet implemented.

dactyl/dactyl_build.py

+93-27
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
# Necessary to copy static files to the output dir
2020
from distutils.dir_util import copy_tree, remove_tree
21+
from shutil import copy as copy_file
2122

2223
# Used for pulling in the default config file
2324
from pkg_resources import resource_stream
@@ -53,6 +54,7 @@
5354
"display_name",
5455
"filters",
5556
"image_subs",
57+
"image_re_subs",
5658
"pages",
5759
]
5860
ADHOC_TARGET = "__ADHOC__"
@@ -191,6 +193,31 @@ def substitute_links_for_target(soup, target):
191193
(local_path, target["image_subs"][local_path]))
192194
img_link["href"] = target["image_subs"][local_path]
193195

196+
if "image_re_subs" in target:
197+
images = soup.find_all("img")
198+
for img in images:
199+
local_path = img["src"]
200+
for regex,replace_pattern in target["image_re_subs"].items():
201+
m = re.match(regex, local_path)
202+
if m:
203+
logging.debug("... matched pattern '%s' for image src '%s'" %
204+
(m, local_path))
205+
new_path = re.sub(regex, replace_pattern, local_path)
206+
logging.debug("... ... replacing with '%s'" % new_path)
207+
img["src"] = new_path
208+
209+
image_links = soup.find_all("a",
210+
href=re.compile(r"^[^.]+\.(png|jpg|jpeg|gif|svg)"))
211+
for img_link in image_links:
212+
local_path = img_link["href"]
213+
for regex,replace_pattern in target["image_re_subs"].items():
214+
m = re.match(regex, local_path)
215+
if m:
216+
logging.debug("... matched pattern '%s' for image link '%s'" %
217+
(m, local_path))
218+
new_path = re.sub(regex, replace_pattern, local_path)
219+
logging.debug("... ... replacing with '%s'" % new_path)
220+
img_link["href"] = new_path
194221

195222
def substitute_parameter_links(link_parameter, currentpage, target):
196223
"""Some templates have links in page parameters. Do link substitution for
@@ -448,7 +475,7 @@ def get_categories(pages):
448475
for page in pages:
449476
if "category" in page and page["category"] not in categories:
450477
categories.append(page["category"])
451-
logger.info("categories: %s" % categories)
478+
logger.debug("categories: %s" % categories)
452479
return categories
453480

454481

@@ -524,15 +551,28 @@ def copy_static_files(template_static=True, content_static=True, out_path=None):
524551
"skipping.") % template_static_src)
525552

526553
if content_static:
527-
content_static_src = config["content_static_path"]
528-
529-
if os.path.isdir(content_static_src):
530-
content_static_dst = os.path.join(out_path,
531-
os.path.basename(content_static_src))
532-
copy_tree(content_static_src, content_static_dst)
533-
else:
534-
logger.warning("Content static path '%s' doesn't exist; skipping." %
554+
if "content_static_path" in config:
555+
if type(config["content_static_path"]) == str:
556+
content_static_srcs = [config["content_static_path"]]
557+
else:
558+
content_static_srcs = config["content_static_path"]
559+
560+
for content_static_src in content_static_srcs:
561+
if os.path.isdir(content_static_src):
562+
content_static_dst = os.path.join(out_path,
563+
os.path.basename(content_static_src))
564+
copy_tree(content_static_src, content_static_dst)
565+
elif os.path.isfile(content_static_src):
566+
content_static_dst = os.path.join(out_path,
567+
os.path.dirname(content_static_src))
568+
logger.debug("Copying single content_static_path file '%s'." %
535569
content_static_src)
570+
copy_file(content_static_src, content_static_dst)
571+
else:
572+
logger.warning("Content static path '%s' doesn't exist; skipping." %
573+
content_static_src)
574+
else:
575+
logger.debug("No content_static_path in conf; skipping copy")
536576

537577

538578
def setup_pp_env(page=None):
@@ -656,10 +696,10 @@ def render_pages(target=None, for_pdf=False, bypass_errors=False):
656696

657697
# Figure out which template to use
658698
if "template" in currentpage and not for_pdf:
659-
logger.info("using template %s from page" % currentpage["template"])
699+
logger.debug("using template %s from page" % currentpage["template"])
660700
use_template = env.get_template(currentpage["template"])
661701
elif "pdf_template" in currentpage and for_pdf:
662-
logger.info("using pdf_template %s from page" % currentpage["pdf_template"])
702+
logger.debug("using pdf_template %s from page" % currentpage["pdf_template"])
663703
use_template = env.get_template(currentpage["pdf_template"])
664704
else:
665705
use_template = default_template
@@ -732,25 +772,34 @@ def make_pdf(outfile, target=None, bypass_errors=False, remove_tmp=True):
732772

733773
temp_files_path = temp_dir()
734774

775+
# Choose a reasonable default filename if one wasn't provided yet
776+
if outfile == DEFAULT_PDF_FILE:
777+
outfile = default_pdf_name(target)
778+
735779
# Prince will need the static files, so copy them over
736780
copy_static_files(out_path=temp_files_path)
737781

738782
# Make sure the path we're going to write the PDF to exists
739783
if not os.path.isdir(config["out_path"]):
740-
logger.info("creating build folder %s" % config["out_path"])
784+
logger.info("creating output folder %s" % config["out_path"])
741785
os.makedirs(config["out_path"])
786+
abs_pdf_path = os.path.abspath(os.path.join(config["out_path"], outfile))
742787

743788
# Start preparing the prince command
744-
args = [config["prince_executable"], '--javascript', '-o', outfile]
789+
args = [config["prince_executable"], '--javascript', '-o', abs_pdf_path]
790+
# Change dir to the tempfiles path; this may avoid a bug in Prince
791+
old_cwd = os.getcwd()
792+
os.chdir(temp_files_path)
745793
# Each HTML output file in the target is another arg to prince
746794
pages = get_pages(target)
747-
args += [os.path.join(temp_files_path, p["html"]) for p in pages]
795+
args += [p["html"] for p in pages]
748796

749797
logger.info("generating PDF: running %s..." % " ".join(args))
750798
prince_resp = subprocess.check_output(args, universal_newlines=True)
751799
print(prince_resp)
752800

753801
# Clean up the tempdir now that we're done using it
802+
os.chdir(old_cwd)
754803
if remove_tmp:
755804
remove_tree(temp_files_path)
756805

@@ -779,7 +828,9 @@ def githubify(md_file_name, target=None):
779828

780829

781830
def main(cli_args):
782-
if not cli_args.quiet:
831+
if cli_args.debug:
832+
logger.setLevel(logging.DEBUG)
833+
elif not cli_args.quiet:
783834
logger.setLevel(logging.INFO)
784835

785836
if cli_args.config:
@@ -815,11 +866,28 @@ def main(cli_args):
815866

816867
target = get_target(cli_args.target)
817868

869+
if cli_args.vars:
870+
try:
871+
if cli_args.vars[-5:] in (".json",".yaml"):
872+
with open(cli_args.vars, "r") as f:
873+
custom_keys = yaml.load(f)
874+
else:
875+
custom_keys = yaml.load(cli_args.vars)
876+
for k,v in custom_keys.items():
877+
if k not in RESERVED_KEYS_TARGET:
878+
logger.debug("setting var '%s'='%s'" %(k,v))
879+
target[k] = v
880+
else:
881+
raise KeyError("Vars can't include reserved key '%s'" % k)
882+
except Exception as e:
883+
traceback.print_tb(e.__traceback__)
884+
exit("FATAL: --vars value was improperly formatted: %s" % e)
885+
818886
if cli_args.title:
819887
target["display_name"] = cli_args.title
820888

821889
if cli_args.githubify:
822-
githubify(cli_args.githubify, cli_args.target)
890+
githubify(cli_args.githubify, target)
823891
if cli_args.copy_static:
824892
copy_static(template_static=False, content_static=True)
825893
exit(0)
@@ -831,15 +899,8 @@ def main(cli_args):
831899
config["pages"].insert(0, coverpage)
832900

833901
if cli_args.pdf != NO_PDF:
834-
if cli_args.pdf == DEFAULT_PDF_FILE:
835-
pdf_path = os.path.join(config["out_path"],
836-
default_pdf_name(cli_args.target))
837-
elif cli_args.pdf[-4:] != ".pdf":
838-
exit("PDF filename must end in .pdf")
839-
else:
840-
pdf_path = os.path.join(config["out_path"], cli_args.pdf)
841902
logger.info("making a pdf...")
842-
make_pdf(pdf_path, target=cli_args.target,
903+
make_pdf(cli_args.pdf, target=target,
843904
bypass_errors=cli_args.bypass_errors,
844905
remove_tmp=(not cli_args.leave_temp_files))
845906
logger.info("pdf done")
@@ -857,10 +918,10 @@ def main(cli_args):
857918
if cli_args.watch:
858919
logger.info("watching for changes...")
859920
if cli_args.pdf != NO_PDF:
860-
pdf_path = os.path.join(config["out_path"], cli_args.pdf)
861-
watch(pdf_path, cli_args.target)
921+
# pdf_path = os.path.join(config["out_path"], cli_args.pdf)
922+
watch(cli_args.pdf, target)
862923
else:
863-
watch(None, cli_args.target)
924+
watch(None, target)
864925

865926

866927
def dispatch_main():
@@ -880,6 +941,8 @@ def dispatch_main():
880941
help="Output to this folder (overrides config file)")
881942
parser.add_argument("--quiet", "-q", action="store_true",
882943
help="Suppress status messages")
944+
parser.add_argument("--debug", action="store_true",
945+
help="Print debug-level log messages (overrides -q)")
883946
parser.add_argument("--bypass_errors", "-b", action="store_true",
884947
help="Continue building if some contents not found")
885948
parser.add_argument("--config", "-c", type=str,
@@ -902,6 +965,9 @@ def dispatch_main():
902965
help="Leave temp files in place (for debugging or "+
903966
"manual PDF generation). Ignored when using --watch",
904967
default=False)
968+
parser.add_argument("--vars", type=str, help="A YAML or JSON file with vars "+
969+
"to add to the target so the preprocessor and "+
970+
"templates can reference them.")
905971
parser.add_argument("--version", "-v", action="store_true",
906972
help="Print version information and exit.")
907973
cli_args = parser.parse_args()

dactyl/default-config.yml

+18-2
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,27 @@
1-
## File paths
1+
## Paths to markdown files are relative to this folder
22
content_path: content
3-
content_static_path: content/static
3+
4+
## Files or folders of images/etc. used in your documents, to be copied to the
5+
## output directory when you use `-s` and to the temp dir when you make pdfs.
6+
## Can be a single path:
7+
#content_static_path: content/static
8+
## ...or an array of paths (files or folders to be copied to the out):
9+
#content_static_path:
10+
# - content/images
11+
# - content/video
12+
# - flow-diagram.png
13+
## If you omit the content_static_path, then no files are copied over.
14+
415
## If no template path is provided, use the ones built into Dactyl
516
#template_path: templates
17+
18+
## Path where the Dactyl output should be written.
619
out_path: out
720

21+
## Static files used in your templates, to be copied to the output directory.
822
template_static_path: assets
23+
24+
## Temporary folder. Dactyl puts temp files in a timestamped subfolder of this.
925
temporary_files_path: /tmp/
1026

1127
## Filters include "badges", "buttonize", "callouts", and more

dactyl/version.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = '0.2.3'
1+
__version__ = '0.3.0'

0 commit comments

Comments
 (0)