Loading .gitignore +0 −1 Original line number Diff line number Diff line *~ *.pyc .DS_Store _site README.html css/swc.css +56 −16 Original line number Diff line number Diff line Loading @@ -8,6 +8,10 @@ h1, h2 { margin-bottom: 10px; } h1:first-child, h2:first-child { margin-top: 10px; } h1 a, h2 a, h3 a, h4 a, h5 a, h6 a { color: inherit; } Loading Loading @@ -111,20 +115,25 @@ h1 a, h2 a, h3 a, h4 a, h5 a, h6 a { color: inherit; } /* Objectives and key points */ /* Objectives, Callout Box and Challenges */ .objectives, .keypoints, .callout, .challenge { margin: 1em 0; padding: 0em 1em; } .objectives, .keypoints { background-color: azure; border: 5px solid azure; margin: 1em 0; padding: 0em 1em; } /* Challenges */ .callout { background-color: #EEE; border: 5px solid #EEE; } .challenge { background-color: #CCFFCC; border: 5px solid #CCFFCC; margin: 1em 0; padding: 0em 1em; } /* Things to fix. */ Loading @@ -146,14 +155,6 @@ blockquote { width: 90%; } /* Callout Box */ .callout { background-color: #EEE; border: 5px solid #EEE; margin: 1em 0; padding: 0em 1em; } /* Tables used for displaying choices in challenges. */ table.choices tr td { vertical-align : top; Loading @@ -165,7 +166,8 @@ table.outlined { } /* Code sample */ pre.sourceCode{ pre.sourceCode, pre.input { color: ForestGreen; } pre.output { Loading @@ -187,6 +189,22 @@ pre.error { line-height: 13pt; } /* Objectives, Callout Box and Challenges */ .objectives, .keypoints { background-color: unset; border: 5px solid; } .callout { background-color: unset; border: 5px solid; } .challenge { background-color: unset; border: 5px solid; } p,ul,ol,li,pre,code { font-size: 8pt; line-height: 9pt; Loading @@ -195,7 +213,29 @@ pre.error { code { padding: 0px; border: 0px; background: none; background: unset; } pre.sourceCode::before, pre.input::before. { content: "Input:"; } pre.output::before { content: "Output:"; } pre.error::before { content: "Error:"; } pre.sourceCode code, pre.input code, pre.output code, pre.error code { display: block; margin-top: 1em; margin-left: 2em; } #github-ribbon { Loading tools/check.py +98 −55 Original line number Diff line number Diff line Loading @@ -10,7 +10,6 @@ Contains validators for several kinds of template. Call at command line with flag -h to see options and usage instructions. """ from __future__ import print_function import argparse import glob Loading Loading @@ -141,7 +140,7 @@ class MarkdownValidator(object): for h in missing_headings: logging.error("In {0}: " "Header section is missing expected " "row {1}".format(self.filename, h)) "row '{1}'".format(self.filename, h)) return has_hrs and all(test_headers) and only_headers Loading Loading @@ -203,20 +202,17 @@ class MarkdownValidator(object): return (len(missing_headings) == 0) and \ valid_order and no_extra and correct_level def _validate_one_link(self, link_node): """Logic to validate a single external asset (image or link) # Link validation methods def _validate_one_html_link(self, link_node, check_text=False): """ Any local html file being linked was generated as part of the lesson. Therefore, file links (.html) must have a Markdown file in the expected folder. The title of the linked Markdown document should match the link text. For other assets (links or images), just verify that a file exists """ dest, link_text = self.ast.get_link_info(link_node) if re.match(r"^[\w,\s-]+\.(html?)", dest, re.IGNORECASE): # HTML files in same folder are made from Markdown; special tests fn = dest.split("#")[0] # Split anchor name from filename expected_md_fn = os.path.splitext(fn)[0] + os.extsep + "md" Loading @@ -227,9 +223,10 @@ class MarkdownValidator(object): "In {0}: " "The document links to {1}, but could not find " "the expected markdown file {2}".format( self.filename, dest, expected_md_path)) self.filename, fn, expected_md_path)) return False if check_text is True: # If file exists, parse and validate link text = node title with open(expected_md_path, 'rU') as link_dest_file: dest_contents = link_dest_file.read() Loading @@ -247,36 +244,67 @@ class MarkdownValidator(object): self.filename, dest, link_text, dest_page_title)) return False elif not re.match(r"^((https?|ftp)://)", dest, re.IGNORECASE)\ return True def _validate_one_link(self, link_node, check_text=False): """Logic to validate a single link to a file asset Performs special checks for links to a local markdown file. For links or images, just verify that a file exists. """ dest, link_text = self.ast.get_link_info(link_node) if re.match(r"^[\w,\s-]+\.(html?)", dest, re.IGNORECASE): # Validate local html links have matching md file return self._validate_one_html_link(link_node, check_text=check_text) elif not re.match(r"^((https?|ftp)://.+)", dest, re.IGNORECASE)\ and not re.match(r"^#.*", dest): # If not web URL, and not anchor on same page, then # verify that local file exists dest_path = os.path.join(self.lesson_dir, dest) dest_path = dest_path.split("#")[0] # Split anchor from filename if not os.path.isfile(dest_path): fn = dest.split("#")[0] # Split anchor name from filename logging.error( "In {0}: " "Could not find the linked asset file " "{1} in {2}. If this is a URL, it must be " "prefixed with http(s):// or ftp://.".format( self.filename, dest, dest_path)) self.filename, fn, dest_path)) return False else: logging.info( logging.debug( "In {0}: " "Skipped validation of link {1}".format(self.filename, dest)) return True def _validate_links(self, links_to_skip=()): def _partition_links(self): """Fetch links in document. If this template has special requirements for link text (eg only some links' text should match dest page title), filter the list accordingly. Default behavior: don't check the text of any links""" check_text = [] no_check_text = self.ast.find_external_links() return check_text, no_check_text def _validate_links(self): """Validate all references to external content This includes links AND images: these are the two types of node that CommonMark assigns a .destination property""" links = self.ast.find_external_links() check_text, no_check_text = self._partition_links() valid = True for link_node in links: if link_node.destination not in links_to_skip: res = self._validate_one_link(link_node) for link_node in check_text: res = self._validate_one_link(link_node, check_text=True) valid = valid and res for link_node in no_check_text: res = self._validate_one_link(link_node, check_text=False) valid = valid and res return valid Loading Loading @@ -309,6 +337,11 @@ class IndexPageValidator(MarkdownValidator): DOC_HEADERS = {'layout': vh.is_str, 'title': vh.is_str} def _partition_links(self): """Check the text of every link in index.md""" check_text = self.ast.find_external_links() return check_text, [] def _validate_intro_section(self): """Validate the intro section Loading Loading @@ -339,12 +372,6 @@ class IndexPageValidator(MarkdownValidator): self.filename)) return intro_section and prereqs_tests def _validate_links(self, links_to_skip=('motivation.html', 'reference.html', 'discussion.html', 'instructors.html')): return super(IndexPageValidator, self)._validate_links(links_to_skip) def _run_tests(self): parent_tests = super(IndexPageValidator, self)._run_tests() tests = [self._validate_intro_section()] Loading @@ -371,9 +398,9 @@ class TopicPageValidator(MarkdownValidator): if node_tests is False: logging.error( "In {0}: " "Learning Objectives should not be empty.".format( self.filename)) "Page should contain a blockquoted section with level 2 " "title 'Learning Objectives'. Section should not " "be empty.".format(self.filename)) return node_tests def _validate_has_no_headings(self): Loading @@ -385,17 +412,13 @@ class TopicPageValidator(MarkdownValidator): if len(heading_nodes) == 0: return True # Individual heading msgs are logged by validate_section_heading_order logging.error( "In {0}: " "The topic page should not have sub-headings " "outside of special blocks. " "If a topic needs sub-headings, " "it should be broken into multiple topics.".format(self.filename)) for n in heading_nodes: logging.warning( "In {0}: " "The following sub-heading should be removed: {1}".format( self.filename, n.strings[0])) return False def _run_tests(self): Loading @@ -422,6 +445,15 @@ class ReferencePageValidator(MarkdownValidator): "title": vh.is_str, "subtitle": vh.is_str} def _partition_links(self): """For reference.md, only check that text of link matches dest page subtitle if the link is in a heading""" all_links = self.ast.find_external_links() check_text = self.ast.find_external_links( parent_crit=self.ast.is_heading) dont_check_text = [n for n in all_links if n not in check_text] return check_text, dont_check_text def _validate_glossary_entry(self, glossary_entry): """Validate glossary entry Loading @@ -433,9 +465,9 @@ class ReferencePageValidator(MarkdownValidator): if len(glossary_entry) < 2: logging.error( "In {0}: " "Glossary entry must have at least two lines- " "Glossary entry '{1}' must have at least two lines- " "a term and a definition.".format( self.filename)) self.filename, glossary_keyword)) return False entry_is_valid = True Loading @@ -444,17 +476,19 @@ class ReferencePageValidator(MarkdownValidator): if not re.match("^: ", line): logging.error( "In {0}: " "At glossary entry '{1}' " "First line of definition must " "start with ': '.".format( self.filename)) self.filename, glossary_keyword)) entry_is_valid = False elif line_index > 1: if not re.match("^ ", line): logging.error( "In {0}: " "At glossary entry '{1}' " "Subsequent lines of definition must " "start with ' '.".format( self.filename)) self.filename, glossary_keyword, )) entry_is_valid = False return entry_is_valid Loading Loading @@ -492,6 +526,15 @@ class InstructorPageValidator(MarkdownValidator): "title": vh.is_str, "subtitle": vh.is_str} def _partition_links(self): """For instructors.md, only check that text of link matches dest page subtitle if the link is in a heading""" all_links = self.ast.find_external_links() check_text = self.ast.find_external_links( parent_crit=self.ast.is_heading) dont_check_text = [n for n in all_links if n not in check_text] return check_text, dont_check_text class LicensePageValidator(MarkdownValidator): """Validate LICENSE.md: user should not edit this file""" Loading tools/test_check.py +54 −0 Original line number Diff line number Diff line Loading @@ -202,6 +202,18 @@ Paragraph of introductory material. self.assertFalse(validator._validate_intro_section()) # TESTS INVOLVING LINKS TO OTHER CONTENT def test_should_check_text_of_all_links_in_index(self): """Text of every local-html link in index.md should match dest page title""" validator = self._create_validator(""" ## [This link is in a heading](reference.html) [Topic Title One](01-one.html#anchor)""") links = validator.ast.find_external_links() check_text, dont_check_text = validator._partition_links() self.assertEqual(len(dont_check_text), 0) self.assertEqual(len(check_text), 2) def test_file_links_validate(self): """Verify that all links in a sample file validate. Involves checking for example files; may fail on "core" branch""" Loading Loading @@ -295,6 +307,26 @@ minutes: not a number ---""") self.assertFalse(validator._validate_doc_headers()) def test_topic_page_should_have_no_headings(self): """Requirement according to spec; may be relaxed in future""" validator = self._create_validator(""" ## Heading that should not be present Some text""") self.assertFalse(validator._validate_has_no_headings()) def test_should_not_check_text_of_links_in_topic(self): """Never check that text of local-html links in topic matches dest title """ validator = self._create_validator(""" ## [This link is in a heading](reference.html) [Topic Title One](01-one.html#anchor)""") links = validator.ast.find_external_links() check_text, dont_check_text = validator._partition_links() self.assertEqual(len(dont_check_text), 2) self.assertEqual(len(check_text), 0) def test_sample_file_passes_validation(self): sample_validator = self.VALIDATOR(self.SAMPLE_FILE) res = sample_validator.validate() Loading Loading @@ -377,6 +409,28 @@ class TestInstructorPage(BaseTemplateTest): SAMPLE_FILE = os.path.join(MARKDOWN_DIR, "instructors.md") VALIDATOR = check.InstructorPageValidator def test_should_selectively_check_text_of_links_in_topic(self): """Only verify that text of local-html links in topic matches dest title if the link is in a heading""" validator = self._create_validator(""" ## [Reference](reference.html) [Topic Title One](01-one.html#anchor)""") check_text, dont_check_text = validator._partition_links() self.assertEqual(len(dont_check_text), 1) self.assertEqual(len(check_text), 1) def test_link_dest_bad_while_text_ignored(self): validator = self._create_validator(""" [ignored text](nonexistent.html)""") self.assertFalse(validator._validate_links()) def test_link_dest_good_while_text_ignored(self): validator = self._create_validator(""" [ignored text](01-one.html)""") self.assertTrue(validator._validate_links()) def test_sample_file_passes_validation(self): sample_validator = self.VALIDATOR(self.SAMPLE_FILE) res = sample_validator.validate() Loading tools/validation_helpers.py +9 −3 Original line number Diff line number Diff line Loading @@ -121,21 +121,27 @@ class CommonMarkHelper(object): return dest, link_text def find_external_links(self, ast_node=None): def find_external_links(self, ast_node=None, parent_crit=None): """Recursive function that locates all references to external content under specified node. (links or images)""" ast_node = ast_node or self.data if parent_crit is None: # User can optionally provide a function to filter link list # based on where link appears. (eg, only links in headings) # If no filter is provided, accept all links in that node. parent_crit = lambda n: True # Link can be node itself, or hiding in inline content links = [n for n in ast_node.inline_content if self.is_external(n)] if self.is_external(n) and parent_crit(ast_node)] if self.is_external(ast_node): links.append(ast_node) # Also look for links in sub-nodes for n in ast_node.children: links.extend(self.find_external_links(n)) links.extend(self.find_external_links(n, parent_crit=parent_crit)) return links Loading Loading
.gitignore +0 −1 Original line number Diff line number Diff line *~ *.pyc .DS_Store _site README.html
css/swc.css +56 −16 Original line number Diff line number Diff line Loading @@ -8,6 +8,10 @@ h1, h2 { margin-bottom: 10px; } h1:first-child, h2:first-child { margin-top: 10px; } h1 a, h2 a, h3 a, h4 a, h5 a, h6 a { color: inherit; } Loading Loading @@ -111,20 +115,25 @@ h1 a, h2 a, h3 a, h4 a, h5 a, h6 a { color: inherit; } /* Objectives and key points */ /* Objectives, Callout Box and Challenges */ .objectives, .keypoints, .callout, .challenge { margin: 1em 0; padding: 0em 1em; } .objectives, .keypoints { background-color: azure; border: 5px solid azure; margin: 1em 0; padding: 0em 1em; } /* Challenges */ .callout { background-color: #EEE; border: 5px solid #EEE; } .challenge { background-color: #CCFFCC; border: 5px solid #CCFFCC; margin: 1em 0; padding: 0em 1em; } /* Things to fix. */ Loading @@ -146,14 +155,6 @@ blockquote { width: 90%; } /* Callout Box */ .callout { background-color: #EEE; border: 5px solid #EEE; margin: 1em 0; padding: 0em 1em; } /* Tables used for displaying choices in challenges. */ table.choices tr td { vertical-align : top; Loading @@ -165,7 +166,8 @@ table.outlined { } /* Code sample */ pre.sourceCode{ pre.sourceCode, pre.input { color: ForestGreen; } pre.output { Loading @@ -187,6 +189,22 @@ pre.error { line-height: 13pt; } /* Objectives, Callout Box and Challenges */ .objectives, .keypoints { background-color: unset; border: 5px solid; } .callout { background-color: unset; border: 5px solid; } .challenge { background-color: unset; border: 5px solid; } p,ul,ol,li,pre,code { font-size: 8pt; line-height: 9pt; Loading @@ -195,7 +213,29 @@ pre.error { code { padding: 0px; border: 0px; background: none; background: unset; } pre.sourceCode::before, pre.input::before. { content: "Input:"; } pre.output::before { content: "Output:"; } pre.error::before { content: "Error:"; } pre.sourceCode code, pre.input code, pre.output code, pre.error code { display: block; margin-top: 1em; margin-left: 2em; } #github-ribbon { Loading
tools/check.py +98 −55 Original line number Diff line number Diff line Loading @@ -10,7 +10,6 @@ Contains validators for several kinds of template. Call at command line with flag -h to see options and usage instructions. """ from __future__ import print_function import argparse import glob Loading Loading @@ -141,7 +140,7 @@ class MarkdownValidator(object): for h in missing_headings: logging.error("In {0}: " "Header section is missing expected " "row {1}".format(self.filename, h)) "row '{1}'".format(self.filename, h)) return has_hrs and all(test_headers) and only_headers Loading Loading @@ -203,20 +202,17 @@ class MarkdownValidator(object): return (len(missing_headings) == 0) and \ valid_order and no_extra and correct_level def _validate_one_link(self, link_node): """Logic to validate a single external asset (image or link) # Link validation methods def _validate_one_html_link(self, link_node, check_text=False): """ Any local html file being linked was generated as part of the lesson. Therefore, file links (.html) must have a Markdown file in the expected folder. The title of the linked Markdown document should match the link text. For other assets (links or images), just verify that a file exists """ dest, link_text = self.ast.get_link_info(link_node) if re.match(r"^[\w,\s-]+\.(html?)", dest, re.IGNORECASE): # HTML files in same folder are made from Markdown; special tests fn = dest.split("#")[0] # Split anchor name from filename expected_md_fn = os.path.splitext(fn)[0] + os.extsep + "md" Loading @@ -227,9 +223,10 @@ class MarkdownValidator(object): "In {0}: " "The document links to {1}, but could not find " "the expected markdown file {2}".format( self.filename, dest, expected_md_path)) self.filename, fn, expected_md_path)) return False if check_text is True: # If file exists, parse and validate link text = node title with open(expected_md_path, 'rU') as link_dest_file: dest_contents = link_dest_file.read() Loading @@ -247,36 +244,67 @@ class MarkdownValidator(object): self.filename, dest, link_text, dest_page_title)) return False elif not re.match(r"^((https?|ftp)://)", dest, re.IGNORECASE)\ return True def _validate_one_link(self, link_node, check_text=False): """Logic to validate a single link to a file asset Performs special checks for links to a local markdown file. For links or images, just verify that a file exists. """ dest, link_text = self.ast.get_link_info(link_node) if re.match(r"^[\w,\s-]+\.(html?)", dest, re.IGNORECASE): # Validate local html links have matching md file return self._validate_one_html_link(link_node, check_text=check_text) elif not re.match(r"^((https?|ftp)://.+)", dest, re.IGNORECASE)\ and not re.match(r"^#.*", dest): # If not web URL, and not anchor on same page, then # verify that local file exists dest_path = os.path.join(self.lesson_dir, dest) dest_path = dest_path.split("#")[0] # Split anchor from filename if not os.path.isfile(dest_path): fn = dest.split("#")[0] # Split anchor name from filename logging.error( "In {0}: " "Could not find the linked asset file " "{1} in {2}. If this is a URL, it must be " "prefixed with http(s):// or ftp://.".format( self.filename, dest, dest_path)) self.filename, fn, dest_path)) return False else: logging.info( logging.debug( "In {0}: " "Skipped validation of link {1}".format(self.filename, dest)) return True def _validate_links(self, links_to_skip=()): def _partition_links(self): """Fetch links in document. If this template has special requirements for link text (eg only some links' text should match dest page title), filter the list accordingly. Default behavior: don't check the text of any links""" check_text = [] no_check_text = self.ast.find_external_links() return check_text, no_check_text def _validate_links(self): """Validate all references to external content This includes links AND images: these are the two types of node that CommonMark assigns a .destination property""" links = self.ast.find_external_links() check_text, no_check_text = self._partition_links() valid = True for link_node in links: if link_node.destination not in links_to_skip: res = self._validate_one_link(link_node) for link_node in check_text: res = self._validate_one_link(link_node, check_text=True) valid = valid and res for link_node in no_check_text: res = self._validate_one_link(link_node, check_text=False) valid = valid and res return valid Loading Loading @@ -309,6 +337,11 @@ class IndexPageValidator(MarkdownValidator): DOC_HEADERS = {'layout': vh.is_str, 'title': vh.is_str} def _partition_links(self): """Check the text of every link in index.md""" check_text = self.ast.find_external_links() return check_text, [] def _validate_intro_section(self): """Validate the intro section Loading Loading @@ -339,12 +372,6 @@ class IndexPageValidator(MarkdownValidator): self.filename)) return intro_section and prereqs_tests def _validate_links(self, links_to_skip=('motivation.html', 'reference.html', 'discussion.html', 'instructors.html')): return super(IndexPageValidator, self)._validate_links(links_to_skip) def _run_tests(self): parent_tests = super(IndexPageValidator, self)._run_tests() tests = [self._validate_intro_section()] Loading @@ -371,9 +398,9 @@ class TopicPageValidator(MarkdownValidator): if node_tests is False: logging.error( "In {0}: " "Learning Objectives should not be empty.".format( self.filename)) "Page should contain a blockquoted section with level 2 " "title 'Learning Objectives'. Section should not " "be empty.".format(self.filename)) return node_tests def _validate_has_no_headings(self): Loading @@ -385,17 +412,13 @@ class TopicPageValidator(MarkdownValidator): if len(heading_nodes) == 0: return True # Individual heading msgs are logged by validate_section_heading_order logging.error( "In {0}: " "The topic page should not have sub-headings " "outside of special blocks. " "If a topic needs sub-headings, " "it should be broken into multiple topics.".format(self.filename)) for n in heading_nodes: logging.warning( "In {0}: " "The following sub-heading should be removed: {1}".format( self.filename, n.strings[0])) return False def _run_tests(self): Loading @@ -422,6 +445,15 @@ class ReferencePageValidator(MarkdownValidator): "title": vh.is_str, "subtitle": vh.is_str} def _partition_links(self): """For reference.md, only check that text of link matches dest page subtitle if the link is in a heading""" all_links = self.ast.find_external_links() check_text = self.ast.find_external_links( parent_crit=self.ast.is_heading) dont_check_text = [n for n in all_links if n not in check_text] return check_text, dont_check_text def _validate_glossary_entry(self, glossary_entry): """Validate glossary entry Loading @@ -433,9 +465,9 @@ class ReferencePageValidator(MarkdownValidator): if len(glossary_entry) < 2: logging.error( "In {0}: " "Glossary entry must have at least two lines- " "Glossary entry '{1}' must have at least two lines- " "a term and a definition.".format( self.filename)) self.filename, glossary_keyword)) return False entry_is_valid = True Loading @@ -444,17 +476,19 @@ class ReferencePageValidator(MarkdownValidator): if not re.match("^: ", line): logging.error( "In {0}: " "At glossary entry '{1}' " "First line of definition must " "start with ': '.".format( self.filename)) self.filename, glossary_keyword)) entry_is_valid = False elif line_index > 1: if not re.match("^ ", line): logging.error( "In {0}: " "At glossary entry '{1}' " "Subsequent lines of definition must " "start with ' '.".format( self.filename)) self.filename, glossary_keyword, )) entry_is_valid = False return entry_is_valid Loading Loading @@ -492,6 +526,15 @@ class InstructorPageValidator(MarkdownValidator): "title": vh.is_str, "subtitle": vh.is_str} def _partition_links(self): """For instructors.md, only check that text of link matches dest page subtitle if the link is in a heading""" all_links = self.ast.find_external_links() check_text = self.ast.find_external_links( parent_crit=self.ast.is_heading) dont_check_text = [n for n in all_links if n not in check_text] return check_text, dont_check_text class LicensePageValidator(MarkdownValidator): """Validate LICENSE.md: user should not edit this file""" Loading
tools/test_check.py +54 −0 Original line number Diff line number Diff line Loading @@ -202,6 +202,18 @@ Paragraph of introductory material. self.assertFalse(validator._validate_intro_section()) # TESTS INVOLVING LINKS TO OTHER CONTENT def test_should_check_text_of_all_links_in_index(self): """Text of every local-html link in index.md should match dest page title""" validator = self._create_validator(""" ## [This link is in a heading](reference.html) [Topic Title One](01-one.html#anchor)""") links = validator.ast.find_external_links() check_text, dont_check_text = validator._partition_links() self.assertEqual(len(dont_check_text), 0) self.assertEqual(len(check_text), 2) def test_file_links_validate(self): """Verify that all links in a sample file validate. Involves checking for example files; may fail on "core" branch""" Loading Loading @@ -295,6 +307,26 @@ minutes: not a number ---""") self.assertFalse(validator._validate_doc_headers()) def test_topic_page_should_have_no_headings(self): """Requirement according to spec; may be relaxed in future""" validator = self._create_validator(""" ## Heading that should not be present Some text""") self.assertFalse(validator._validate_has_no_headings()) def test_should_not_check_text_of_links_in_topic(self): """Never check that text of local-html links in topic matches dest title """ validator = self._create_validator(""" ## [This link is in a heading](reference.html) [Topic Title One](01-one.html#anchor)""") links = validator.ast.find_external_links() check_text, dont_check_text = validator._partition_links() self.assertEqual(len(dont_check_text), 2) self.assertEqual(len(check_text), 0) def test_sample_file_passes_validation(self): sample_validator = self.VALIDATOR(self.SAMPLE_FILE) res = sample_validator.validate() Loading Loading @@ -377,6 +409,28 @@ class TestInstructorPage(BaseTemplateTest): SAMPLE_FILE = os.path.join(MARKDOWN_DIR, "instructors.md") VALIDATOR = check.InstructorPageValidator def test_should_selectively_check_text_of_links_in_topic(self): """Only verify that text of local-html links in topic matches dest title if the link is in a heading""" validator = self._create_validator(""" ## [Reference](reference.html) [Topic Title One](01-one.html#anchor)""") check_text, dont_check_text = validator._partition_links() self.assertEqual(len(dont_check_text), 1) self.assertEqual(len(check_text), 1) def test_link_dest_bad_while_text_ignored(self): validator = self._create_validator(""" [ignored text](nonexistent.html)""") self.assertFalse(validator._validate_links()) def test_link_dest_good_while_text_ignored(self): validator = self._create_validator(""" [ignored text](01-one.html)""") self.assertTrue(validator._validate_links()) def test_sample_file_passes_validation(self): sample_validator = self.VALIDATOR(self.SAMPLE_FILE) res = sample_validator.validate() Loading
tools/validation_helpers.py +9 −3 Original line number Diff line number Diff line Loading @@ -121,21 +121,27 @@ class CommonMarkHelper(object): return dest, link_text def find_external_links(self, ast_node=None): def find_external_links(self, ast_node=None, parent_crit=None): """Recursive function that locates all references to external content under specified node. (links or images)""" ast_node = ast_node or self.data if parent_crit is None: # User can optionally provide a function to filter link list # based on where link appears. (eg, only links in headings) # If no filter is provided, accept all links in that node. parent_crit = lambda n: True # Link can be node itself, or hiding in inline content links = [n for n in ast_node.inline_content if self.is_external(n)] if self.is_external(n) and parent_crit(ast_node)] if self.is_external(ast_node): links.append(ast_node) # Also look for links in sub-nodes for n in ast_node.children: links.extend(self.find_external_links(n)) links.extend(self.find_external_links(n, parent_crit=parent_crit)) return links Loading