|
1 | | -#!/usr/bin/env bash |
| 1 | +#!/usr/bin/python |
| 2 | +# |
| 3 | +# Software Carpentry Lesson Validator |
| 4 | +# |
| 5 | +# Check for errors in lessons built using the Software Carpentry template |
| 6 | +# found at http://github.com/swcarpentry/lesson-template. |
| 7 | +# |
| 8 | +# Usage: |
| 9 | +# |
| 10 | +# $ tools/check |
2 | 11 |
|
3 | | -# Placeholder for actual conformance checking script (which will |
4 | | -# probably be Python, not Bash). |
| 12 | +import sys |
| 13 | +import os |
| 14 | +import re |
| 15 | +import yaml |
5 | 16 |
|
6 | | -grep -i -n 'FIX''ME' $* |
| 17 | +#---------------------------------------- |
| 18 | +# Error reporting. |
| 19 | + |
| 20 | +def report_error(file_path, line_number, line, error_message): |
| 21 | + """ |
| 22 | + Print information about general error. |
| 23 | + """ |
| 24 | + ERR_MSG = "Error at line {} of {}:\n\t{}\n{}" |
| 25 | + print(ERR_MSG.format(line_number, file_path, line, error_message)) |
| 26 | + |
| 27 | +def report_missing(present, file_path, missing_element): |
| 28 | + """ |
| 29 | + Print information about missing element. |
| 30 | + """ |
| 31 | + ERR_MSG = "Error on {}: missing {}" |
| 32 | + if not present: |
| 33 | + print(ERR_MSG.format(file_path, missing_element)) |
| 34 | + |
| 35 | +def report_missing_metadata(missing_element): |
| 36 | + """ |
| 37 | + Print information about missing metadata at YAML header. |
| 38 | + """ |
| 39 | + ERR_MSG = "Error on YAML header: missing {}" |
| 40 | + print(ERR_MSG.format(missing_element)) |
| 41 | + |
| 42 | +def report_broken_link(file_path, line_number, link): |
| 43 | + """ |
| 44 | + Print information about broken link. |
| 45 | + """ |
| 46 | + ERR_MSG = "Broken link at line {} of {}:\n\tCan't find {}." |
| 47 | + print(ERR_MSG.format(line_number, file_path, link)) |
| 48 | + |
| 49 | +#---------------------------------------- |
| 50 | +# Checking. |
| 51 | + |
| 52 | +def check_yaml(metadata): |
| 53 | + """ |
| 54 | + Check if all metadata are present at YAML header. |
| 55 | + """ |
| 56 | + METADATA_REQUIRED = {"layout", "title", "minutes"} |
| 57 | + for key in METADATA_REQUIRED - set(metadata.keys()): |
| 58 | + report_missing_metadata(key) |
| 59 | + |
| 60 | +# TODO: Implement check_lesson |
| 61 | +def check_lesson(file_path): |
| 62 | + """ |
| 63 | + Checks the file ``pages/[0-9]{2}-.*.md`` for: |
| 64 | +
|
| 65 | + - "layout: topic" in YAML header |
| 66 | + - "title" as keyword in YAML header |
| 67 | + - line "> ## Learning Objectives {.objectives}" after YAML header |
| 68 | + - items in learning objectives begin with "*" |
| 69 | + - items in learning objective following four-space indentation rule |
| 70 | + - code samples be of type input, error, output, python, shell, r, matlab, or sql |
| 71 | + - callout box style |
| 72 | + - challenge box style |
| 73 | + """ |
| 74 | + pass |
| 75 | + |
| 76 | +# TODO: Implement check_discussion |
| 77 | +def check_discussion(file_path): |
| 78 | + """ |
| 79 | + Checks the file ``pages/discussion.md`` for: |
| 80 | +
|
| 81 | + FIXME: tell what need to check. |
| 82 | + """ |
| 83 | + pass |
| 84 | + |
| 85 | +# TODO: Complete implementation of check_index |
| 86 | +# TODO: break check_index into pieces -- it's too long. |
| 87 | +def check_index(file_path): |
| 88 | + """ |
| 89 | + Checks the file ``pages/index.md`` for: |
| 90 | +
|
| 91 | + - "layout: lesson" in YAML header |
| 92 | + - "title" as keyword in YAML header |
| 93 | + - introductory paragraph(s) right after YAML header |
| 94 | + - line with "> ## Prerequisites" |
| 95 | + - non-empty prerequisites |
| 96 | + - title line with "## Topics" |
| 97 | + - items at topic list begin with "*" |
| 98 | + - items in topic list follow four-space indentation rule |
| 99 | + - links at topic list are valid |
| 100 | + - line with "## Other Resources" |
| 101 | + - items at other resources list begin with "*" |
| 102 | + - link at other resources list are valid |
| 103 | + """ |
| 104 | + # State variables |
| 105 | + in_yaml = False |
| 106 | + yaml_metadata = [] |
| 107 | + has_prerequisites = False |
| 108 | + has_topics = False |
| 109 | + has_other_resources = False |
| 110 | + |
| 111 | + # Load file and process it |
| 112 | + with open(file_path, "r") as lines: |
| 113 | + for line_number, line in enumerate(lines): |
| 114 | + if re.match("---", line): # what if there are multiple YAML blocks?? |
| 115 | + in_yaml = not in_yaml |
| 116 | + elif in_yaml: |
| 117 | + yaml_metadata.append(line) |
| 118 | + elif re.match("> ## Prerequisites", line): # check this in the Markdown or in the generated HTML? |
| 119 | + has_prerequisites = True |
| 120 | + elif re.match("## Topics", line): # as above? |
| 121 | + has_topics = True |
| 122 | + elif re.match("## Other Resources", line): # as above |
| 123 | + has_other_resources = True |
| 124 | + else: |
| 125 | + ## Push this check into another function - this one is getting too long. |
| 126 | + # Check if local links are valid |
| 127 | + matches = re.search("\[.*\]\((?P<link>.*)\)", line) |
| 128 | + if matches and not matches.group("link").startswith("http"): |
| 129 | + link = os.path.join(os.path.dirname(file_path), matches.group("link")) |
| 130 | + if link.endswith(".html"): |
| 131 | + link = link.replace("html", "md") # NO: what about "03-html-editing.html" ? |
| 132 | + if not os.path.exists(link): |
| 133 | + report_broken_link(file_path, line_number, link) |
| 134 | + |
| 135 | + ## Again, this function is too long - break it into sub-functions. |
| 136 | + # Check YAML |
| 137 | + yaml_metadata = yaml.load("\n".join(yaml_metadata)) |
| 138 | + check_yaml(yaml_metadata) |
| 139 | + |
| 140 | + # Check sections |
| 141 | + ## Note the refactoring: replaces three conditionals with one. |
| 142 | + report_missing(has_prerequisites, file_path, "Prerequisites") |
| 143 | + report_missing(has_topics, file_path, "Topics") |
| 144 | + report_missing(has_other_resources, file_path, "Other Resources") |
| 145 | + |
| 146 | +# TODO Implement check_intructors |
| 147 | +def check_intructors(file_path): |
| 148 | + """ |
| 149 | + Checks the file ``pages/instructors.md`` for: |
| 150 | +
|
| 151 | + - "title: Instructor"s Guide" in YAML header |
| 152 | + - line with "## Overall" |
| 153 | + - line with "## General Points" |
| 154 | + - lines with topics titles begin with "## " |
| 155 | + - points begin with "*" and following four space rules. |
| 156 | + """ |
| 157 | + pass |
| 158 | + |
| 159 | +# TODO Implement check_motivation |
| 160 | +def check_motivation(file_path): |
| 161 | + """ |
| 162 | + Checks the file ``pages/motivation.md``. |
| 163 | +
|
| 164 | + FIXME: tell what need to check. |
| 165 | + """ |
| 166 | + pass |
| 167 | + |
| 168 | +# TODO Implement check_reference |
| 169 | +def check_reference(file_path): |
| 170 | + """ |
| 171 | + Checks the file ``pages/reference.md`` for: |
| 172 | +
|
| 173 | + - ``layout: reference`` in YAML header |
| 174 | + - line with "## Glossary" |
| 175 | + - words definitions after at the "Glossary" as:: |
| 176 | +
|
| 177 | + > **Key Word 1**: the definition |
| 178 | + > relevant to the lesson. |
| 179 | + """ |
| 180 | + pass |
| 181 | + |
| 182 | +def check_file(file_path): |
| 183 | + """ |
| 184 | + Call the correctly check function based on the name of the file. |
| 185 | + """ |
| 186 | + # Pair of regex and function to call |
| 187 | + CONTROL = ( |
| 188 | + ("[0-9]{2}-.*", check_lesson), |
| 189 | + ("discussion", check_discussion), |
| 190 | + ("index", check_index), |
| 191 | + ("instructors", check_intructors), |
| 192 | + ("motivation", check_motivation), |
| 193 | + ("reference", check_reference) |
| 194 | + ) |
| 195 | + for (pattern, checker) in CONTROL: |
| 196 | + if re.search(pattern, file_path): |
| 197 | + checker(file_path) |
| 198 | + |
| 199 | +def main(list_of_files): |
| 200 | + """ |
| 201 | + Call the check function for every file in ``list_of_files``. |
| 202 | +
|
| 203 | + If ``list_of_files`` is empty load all the files from ``pages`` directory. |
| 204 | + """ |
| 205 | + if not list_of_files: |
| 206 | + list_of_files = [os.path.join("pages", filename) for filename in os.listdir("pages")] |
| 207 | + |
| 208 | + for filename in list_of_files: |
| 209 | + if filename.endswith(".md"): |
| 210 | + check_file(filename) |
| 211 | + |
| 212 | +if __name__ == "__main__": |
| 213 | + main(sys.argv[1:]) |
0 commit comments