diff --git a/_layouts/page.html b/_layouts/page.html index 0035fb4a739b69943d38db19036a5b7d83aad0bb..6607cab01409b2f0e19e88aa022e90da2c012040 100644 --- a/_layouts/page.html +++ b/_layouts/page.html @@ -13,7 +13,7 @@ <div class="span10 offset1"> <h1 class="title">$title$</h1> $if(subtitle)$<h2 class="subtitle">$subtitle$</h2>$endif$ - $body$ +$body$ </div> </div> $footer$ diff --git a/_layouts/slides.html b/_layouts/slides.html index 581bc990d13dcd9f1a61e336f5cee60a9342aa43..61718627f9fcd65dbed88665a3cfaeecb9818242 100644 --- a/_layouts/slides.html +++ b/_layouts/slides.html @@ -33,7 +33,7 @@ <div class="deck-container"> <!-- Begin slides. Just make elements with a class of slide. --> - $body$ +$body$ <!-- End slides. --> <!-- Begin extension snippets. Add or remove as needed. --> diff --git a/pages/01-one.md b/pages/01-one.md index 9857e506196cf33aa47efc55270ebbe129dc1e0e..ca4dc268d6b1539b9c418bc9c33993ebebd9c70e 100644 --- a/pages/01-one.md +++ b/pages/01-one.md @@ -4,7 +4,7 @@ title: Lesson Title subtitle: Topic Title One minutes: 10 --- -> ## Learning Objectives {.objectives} +> ## Learning Objectives > > * Learning objective 1 > * Learning objective 2 diff --git a/pages/02-two.md b/pages/02-two.md index b3fdb641a980390f5adf514f8f38b8baaceafeb0..0412d1bb186e0dc2d7bba0c31f4d1b0c242f6441 100644 --- a/pages/02-two.md +++ b/pages/02-two.md @@ -4,7 +4,7 @@ title: Lesson Title subtitle: Topic Title Two minutes: 10 --- -> ## Learning Objectives {.objectives} +> ## Learning Objectives > > * Learning objective 1 > * Learning objective 2 diff --git a/tools/blockquote2div.py b/tools/blockquote2div.py old mode 100644 new mode 100755 index a23109f076b77efe474ed58fceee748fdabb5fc1..6797c86472c92a6526d6f572d0d424f9b2a24638 --- a/tools/blockquote2div.py +++ b/tools/blockquote2div.py @@ -1,3 +1,4 @@ +#!/usr/bin/env python """Pandoc filter to convert Blockquotes with attributes into Div with attributes. @@ -7,10 +8,11 @@ Usage: A blockquote will be converted if -1. it begins with a header -2. that header has attributes -3. those attributes contain a single class -4. that class is one of ['objectives', 'callout', 'challenge'] +1. it begins with a header +2. that either + 1. matches "Prerequisites", "Objectives", "Callout" or "Challenge" OR + 2. has attributes containing a single class matching + one of ['prereq', 'objectives', 'callout', 'challenge'] For example, this is a valid blockquote: @@ -24,6 +26,18 @@ and it will be converted into this markdown: Let's do something. </div> +This is also a valid blockquote: + + > ## Prerequisites + > Breakfast! + +and it will be converted into this markdown: + + <div class='prereq'> + ## Prerequisites + Breakfast! + </div> + For debugging purposes you may find it useful to test the filter like this: @@ -33,10 +47,21 @@ like this: import pandocfilters as pf -valid_classes = ['objectives', 'callout', 'challenge'] +# These are classes that, if set on the title of a blockquote, will +# trigger the blockquote to be converted to a div. +SPECIAL_CLASSES = ['callout', 'challenge', 'prereq', 'objectives'] +# These are titles of blockquotes that will cause the blockquote to +# be converted into a div. They are 'title': 'class' pairs, where the +# 'title' will create a blockquote with the corresponding 'class'. +SPECIAL_TITLES = {'prerequisites': 'prereq', + 'learning objectives': 'objectives', + 'objectives': 'objectives', + 'challenge': 'challenge', + 'callout': 'callout'} -def find_attributes(blockquote): + +def find_header(blockquote): """Find attributes in a blockquote if they are defined on a header that is the first thing in the block quote. @@ -45,7 +70,7 @@ def find_attributes(blockquote): """ if blockquote[0]['t'] == 'Header': level, attr, inline = blockquote[0]['c'] - return attr + return level, attr, inline def remove_attributes(blockquote): @@ -70,10 +95,21 @@ def blockquote2div(key, value, format, meta): """ if key == 'BlockQuote': blockquote = value - attr = find_attributes(blockquote) - if not attr: + + header = find_header(blockquote) + if not header: return - elif len(attr[1]) == 1 and attr[1][0] in valid_classes: + else: + level, attr, inlines = header + + id, classes, kvs = attr + + ltitle = pf.stringify(inlines).lower() + if ltitle in SPECIAL_TITLES: + classes.append(SPECIAL_TITLES[ltitle]) + return pf.Div(attr, blockquote) + + elif len(classes) == 1 and classes[0] in SPECIAL_CLASSES: remove_attributes(blockquote) # a blockquote is just a list of blocks, so it can be # passed directly to Div, which expects Div(attr, blocks) diff --git a/tools/check b/tools/check index e750ec7633a97f41d1e3c440b2bc1f5d149f882a..5457d73910a65ded713ad7ff3599396928c6ecba 100755 --- a/tools/check +++ b/tools/check @@ -1,6 +1,213 @@ -#!/usr/bin/env bash +#!/usr/bin/python +# +# Software Carpentry Lesson Validator +# +# Check for errors in lessons built using the Software Carpentry template +# found at http://github.com/swcarpentry/lesson-template. +# +# Usage: +# +# $ tools/check -# Placeholder for actual conformance checking script (which will -# probably be Python, not Bash). +import sys +import os +import re +import yaml -grep -i -n 'FIX''ME' $* +#---------------------------------------- +# Error reporting. + +def report_error(file_path, line_number, line, error_message): + """ + Print information about general error. + """ + ERR_MSG = "Error at line {} of {}:\n\t{}\n{}" + print(ERR_MSG.format(line_number, file_path, line, error_message)) + +def report_missing(present, file_path, missing_element): + """ + Print information about missing element. + """ + ERR_MSG = "Error on {}: missing {}" + if not present: + print(ERR_MSG.format(file_path, missing_element)) + +def report_missing_metadata(missing_element): + """ + Print information about missing metadata at YAML header. + """ + ERR_MSG = "Error on YAML header: missing {}" + print(ERR_MSG.format(missing_element)) + +def report_broken_link(file_path, line_number, link): + """ + Print information about broken link. + """ + ERR_MSG = "Broken link at line {} of {}:\n\tCan't find {}." + print(ERR_MSG.format(line_number, file_path, link)) + +#---------------------------------------- +# Checking. + +def check_yaml(metadata): + """ + Check if all metadata are present at YAML header. + """ + METADATA_REQUIRED = {"layout", "title", "minutes"} + for key in METADATA_REQUIRED - set(metadata.keys()): + report_missing_metadata(key) + +# TODO: Implement check_lesson +def check_lesson(file_path): + """ + Checks the file ``pages/[0-9]{2}-.*.md`` for: + + - "layout: topic" in YAML header + - "title" as keyword in YAML header + - line "> ## Learning Objectives {.objectives}" after YAML header + - items in learning objectives begin with "*" + - items in learning objective following four-space indentation rule + - code samples be of type input, error, output, python, shell, r, matlab, or sql + - callout box style + - challenge box style + """ + pass + +# TODO: Implement check_discussion +def check_discussion(file_path): + """ + Checks the file ``pages/discussion.md`` for: + + FIXME: tell what need to check. + """ + pass + +# TODO: Complete implementation of check_index +# TODO: break check_index into pieces -- it's too long. +def check_index(file_path): + """ + Checks the file ``pages/index.md`` for: + + - "layout: lesson" in YAML header + - "title" as keyword in YAML header + - introductory paragraph(s) right after YAML header + - line with "> ## Prerequisites" + - non-empty prerequisites + - title line with "## Topics" + - items at topic list begin with "*" + - items in topic list follow four-space indentation rule + - links at topic list are valid + - line with "## Other Resources" + - items at other resources list begin with "*" + - link at other resources list are valid + """ + # State variables + in_yaml = False + yaml_metadata = [] + has_prerequisites = False + has_topics = False + has_other_resources = False + + # Load file and process it + with open(file_path, "r") as lines: + for line_number, line in enumerate(lines): + if re.match("---", line): # what if there are multiple YAML blocks?? + in_yaml = not in_yaml + elif in_yaml: + yaml_metadata.append(line) + elif re.match("> ## Prerequisites", line): # check this in the Markdown or in the generated HTML? + has_prerequisites = True + elif re.match("## Topics", line): # as above? + has_topics = True + elif re.match("## Other Resources", line): # as above + has_other_resources = True + else: + ## Push this check into another function - this one is getting too long. + # Check if local links are valid + matches = re.search("\[.*\]\((?P<link>.*)\)", line) + if matches and not matches.group("link").startswith("http"): + link = os.path.join(os.path.dirname(file_path), matches.group("link")) + if link.endswith(".html"): + link = link.replace("html", "md") # NO: what about "03-html-editing.html" ? + if not os.path.exists(link): + report_broken_link(file_path, line_number, link) + + ## Again, this function is too long - break it into sub-functions. + # Check YAML + yaml_metadata = yaml.load("\n".join(yaml_metadata)) + check_yaml(yaml_metadata) + + # Check sections + ## Note the refactoring: replaces three conditionals with one. + report_missing(has_prerequisites, file_path, "Prerequisites") + report_missing(has_topics, file_path, "Topics") + report_missing(has_other_resources, file_path, "Other Resources") + +# TODO Implement check_intructors +def check_intructors(file_path): + """ + Checks the file ``pages/instructors.md`` for: + + - "title: Instructor"s Guide" in YAML header + - line with "## Overall" + - line with "## General Points" + - lines with topics titles begin with "## " + - points begin with "*" and following four space rules. + """ + pass + +# TODO Implement check_motivation +def check_motivation(file_path): + """ + Checks the file ``pages/motivation.md``. + + FIXME: tell what need to check. + """ + pass + +# TODO Implement check_reference +def check_reference(file_path): + """ + Checks the file ``pages/reference.md`` for: + + - ``layout: reference`` in YAML header + - line with "## Glossary" + - words definitions after at the "Glossary" as:: + + > **Key Word 1**: the definition + > relevant to the lesson. + """ + pass + +def check_file(file_path): + """ + Call the correctly check function based on the name of the file. + """ + # Pair of regex and function to call + CONTROL = ( + ("[0-9]{2}-.*", check_lesson), + ("discussion", check_discussion), + ("index", check_index), + ("instructors", check_intructors), + ("motivation", check_motivation), + ("reference", check_reference) + ) + for (pattern, checker) in CONTROL: + if re.search(pattern, file_path): + checker(file_path) + +def main(list_of_files): + """ + Call the check function for every file in ``list_of_files``. + + If ``list_of_files`` is empty load all the files from ``pages`` directory. + """ + if not list_of_files: + list_of_files = [os.path.join("pages", filename) for filename in os.listdir("pages")] + + for filename in list_of_files: + if filename.endswith(".md"): + check_file(filename) + +if __name__ == "__main__": + main(sys.argv[1:])