Skip to content

Commit 06237e9

Browse files
committed
Merge branch 'r-gaia-cs-check' into gh-pages
2 parents e71f43d + 875a796 commit 06237e9

File tree

1 file changed

+211
-4
lines changed

1 file changed

+211
-4
lines changed

tools/check

Lines changed: 211 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,213 @@
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
211

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
516

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

Comments
 (0)