def test_mathjax_with_valid_defaults(self): """Check if mathematics is rendered correctly with defaults.""" pandoc_default_files = [ os.path.join(TEST_DEFAULT_FILES_PATH, "valid_defaults.yaml") ] settings = get_settings(PANDOC_DEFAULT_FILES=pandoc_default_files) pandoc_reader = PandocReader(settings) source_path = os.path.join(TEST_CONTENT_PATH, "mathjax_content.md") output, metadata = pandoc_reader.read(source_path) self.assertEqual( ( '<p><span class="math display">\\[\n' "e^{i\\theta} = \\cos\\theta + i \\sin\\theta.\n" "\\]</span></p>" ), output, ) self.assertEqual("MathJax Content", str(metadata["title"])) self.assertEqual("My Author", str(metadata["author"])) self.assertEqual("2020-10-16 00:00:00", str(metadata["date"]))
def test_encoded_to_raw_conversion(self): """Check if raw paths are left untouched in output returned.""" settings = get_settings(PANDOC_EXTENSIONS=PANDOC_EXTENSIONS, PANDOC_ARGS=PANDOC_ARGS) pandoc_reader = PandocReader(settings) source_path = os.path.join(TEST_CONTENT_PATH, "valid_content_with_raw_paths.md") output, metadata = pandoc_reader.read(source_path) # Setting this so that assert is able to execute the difference self.maxDiff = None # pylint: disable=invalid-name self.assertEqual( ("<p>This is some valid content that should pass." " If it does not pass we will know something is wrong.</p>\n" "<p>Our fictitious internal files are available" ' <a href="{filename}/path/to/file">at</a>:</p>\n' "<p>Our fictitious static files are available" ' <a href="{static}/path/to/file">at</a>:</p>\n' "<p>Our fictitious attachments are available" ' <a href="{attach}path/to/file">at</a>:</p>'), output, ) self.assertEqual("Valid Content with Fictitious Raw Paths", str(metadata["title"])) self.assertEqual("My Author", str(metadata["author"])) self.assertEqual("2020-10-16 00:00:00", str(metadata["date"]))
def read(self, filename): """Parse content and metadata of markdown files""" QUIET = self.settings.get('RMD_READER_KNITR_QUIET', True) ENCODING = self.settings.get('RMD_READER_KNITR_ENCODING', 'UTF-8') CLEANUP = self.settings.get('RMD_READER_CLEANUP', True) RENAME_PLOT = self.settings.get('RMD_READER_RENAME_PLOT', 'chunklabel') site_url = self.settings.get('SITEURL', '') if type(RENAME_PLOT) is bool: logger.error( "RMD_READER_RENAME_PLOT takes a string value (either chunklabel or directory), please see the readme." ) if RENAME_PLOT: RENAME_PLOT = 'chunklabel' logger.error("Defaulting to chunklabel") else: RENAME_PLOT = 'disabled' logger.error("Disabling plot renaming") logger.debug("RMD_READER_KNITR_QUIET = %s", QUIET) logger.debug("RMD_READER_KNITR_ENCODING = %s", ENCODING) logger.debug("RMD_READER_CLEANUP = %s", CLEANUP) logger.debug("RMD_READER_RENAME_PLOT = %s", RENAME_PLOT) # replace single backslashes with double backslashes filename = filename.replace('\\', '\\\\') # parse Rmd file - generate md file md_filename = filename.replace('.Rmd', '.aux').replace('.rmd', '.aux') if RENAME_PLOT == 'chunklabel' or RENAME_PLOT == 'directory': if RENAME_PLOT == 'chunklabel': chunk_label = os.path.splitext(os.path.basename(filename))[0] logger.debug('Chunk label: %s', chunk_label) elif RENAME_PLOT == 'directory': chunk_label = 'unnamed-chunk' PATH = self.settings.get( 'PATH', '%s/content' % settings.DEFAULT_CONFIG.get('PATH')) src_name = os.path.splitext(os.path.relpath(filename, PATH))[0] idx = KNITR.opts_chunk.names.index('set') knitroptschunk = { 'fig.path': '%s-' % os.path.join(FIG_PATH, src_name) } KNITR.opts_chunk[idx]( **{str(k): v for k, v in knitroptschunk.items()}) logger.debug('Figures path: %s, chunk label: %s', knitroptschunk['fig.path'], chunk_label) R_OBJECTS.r(''' opts_knit$set(unnamed.chunk.label="{unnamed_chunk_label}") render_markdown() hook_plot <- knit_hooks$get('plot') knit_hooks$set(plot=function(x, options) hook_plot(paste0("{SITEURL}/", x), options)) '''.format(unnamed_chunk_label=chunk_label, SITEURL=site_url)) with warnings.catch_warnings(): warnings.simplefilter("ignore") KNITR.knit(filename, md_filename, quiet=QUIET, encoding=ENCODING) # read md file - create a MarkdownReader # md_reader = readers.MarkdownReader(self.settings) md_reader = PandocReader(self.settings) content, metadata = md_reader.read(md_filename) # remove md file if CLEANUP: os.remove(md_filename) return content, metadata
def test_empty_file(self): """Check if a file is empty.""" settings = get_settings(PANDOC_EXTENSIONS=PANDOC_EXTENSIONS, PANDOC_ARGS=PANDOC_ARGS) pandoc_reader = PandocReader(settings) source_path = os.path.join(TEST_CONTENT_PATH, "empty.md") # If the file is empty retrieval of metadata should fail with self.assertRaises(Exception) as context_manager: pandoc_reader.read(source_path) message = str(context_manager.exception) self.assertEqual("Could not find metadata. File is empty.", message)
def test_non_empty_file_no_metadata(self): """Check if a file has no metadata.""" settings = get_settings(PANDOC_EXTENSIONS=PANDOC_EXTENSIONS, PANDOC_ARGS=PANDOC_ARGS) pandoc_reader = PandocReader(settings) source_path = os.path.join(TEST_CONTENT_PATH, "no_metadata.md") # If the file is not empty but has no metadata it should fail with self.assertRaises(Exception) as context_manager: pandoc_reader.read(source_path) message = str(context_manager.exception) self.assertEqual("Could not find metadata header '---'.", message)
def test_invalid_metadata_block_end(self): """Check if the metadata block end is wrong.""" settings = get_settings(PANDOC_EXTENSIONS=PANDOC_EXTENSIONS, PANDOC_ARGS=PANDOC_ARGS) pandoc_reader = PandocReader(settings) source_path = os.path.join(TEST_CONTENT_PATH, "no_metadata_end.md") # Metadata blocks should end with '___' or '...' if not it should fail with self.assertRaises(Exception) as context_manager: pandoc_reader.read(source_path) message = str(context_manager.exception) self.assertEqual("Could not find end of metadata block.", message)
def test_default_wpm_reading_time(self): """Check if 200 words per minute give us reading time of 1 minute.""" settings = get_settings( PANDOC_EXTENSIONS=PANDOC_EXTENSIONS, PANDOC_ARGS=PANDOC_ARGS, CALCULATE_READING_TIME=CALCULATE_READING_TIME, ) pandoc_reader = PandocReader(settings) source_path = os.path.join(TEST_CONTENT_PATH, "reading_time_content.md") _, metadata = pandoc_reader.read(source_path) self.assertEqual("1 minute", str(metadata["reading_time"]))
def test_toc_with_valid_defaults(self): """Check if output and table of contents are valid with defaults.""" pandoc_default_files = [ os.path.join(TEST_DEFAULT_FILES_PATH, "valid_defaults_with_toc.yaml") ] settings = get_settings(PANDOC_DEFAULT_FILES=pandoc_default_files) pandoc_reader = PandocReader(settings) source_path = os.path.join(TEST_CONTENT_PATH, "valid_content_with_toc.md") output, metadata = pandoc_reader.read(source_path) self.maxDiff = None # pylint: disable=invalid-name self.assertEqual( ( "<p>This is some valid content that should pass." " If it does not pass we will know something is wrong.</p>\n" '<h2 id="first-heading">First Heading</h2>\n' "<p>This should be the first heading in my" " table of contents.</p>\n" '<h2 id="second-heading">Second Heading</h2>\n' "<p>This should be the second heading in my" " table of contents.</p>\n" '<h3 id="first-subheading">First Subheading</h3>\n' "<p>This is a subsection that should be shown as such" " in the table of contents.</p>\n" '<h3 id="second-subheading">Second Subheading</h3>\n' "<p>This is another subsection that should be shown as" " such in the table of contents.</p>" ), output, ) self.assertEqual("Valid Content with Table of Contents", str(metadata["title"])) self.assertEqual("My Author", str(metadata["author"])) self.assertEqual("2020-10-16 00:00:00", str(metadata["date"])) self.assertEqual( '<nav class="toc" role="doc-toc">\n' "<ul>\n" '<li><a href="#first-heading">First Heading</a></li>\n' '<li><a href="#second-heading">Second Heading</a>\n' "<ul>\n" '<li><a href="#first-subheading">First Subheading</a></li>\n' '<li><a href="#second-subheading">Second Subheading</a></li>\n' "</ul></li>\n" "</ul>\n" "</nav>", str(metadata["toc"]), )
def test_valid_content_with_toc_1(self): """Check if output returned is valid and table of contents is valid.""" settings = get_settings( PANDOC_EXTENSIONS=PANDOC_EXTENSIONS, PANDOC_ARGS=PANDOC_ARGS + ["--toc"], ) pandoc_reader = PandocReader(settings) source_path = os.path.join(TEST_CONTENT_PATH, "valid_content_with_toc.md") output, metadata = pandoc_reader.read(source_path) # Setting this so that assert is able to execute the difference self.maxDiff = None # pylint: disable=invalid-name self.assertEqual( ("<p>This is some valid content that should pass." " If it does not pass we will know something is wrong.</p>\n" '<h2 id="first-heading">First Heading</h2>\n' "<p>This should be the first heading in my" " table of contents.</p>\n" '<h2 id="second-heading">Second Heading</h2>\n' "<p>This should be the second heading in my" " table of contents.</p>\n" '<h3 id="first-subheading">First Subheading</h3>\n' "<p>This is a subsection that should be shown as such" " in the table of contents.</p>\n" '<h3 id="second-subheading">Second Subheading</h3>\n' "<p>This is another subsection that should be shown as" " such in the table of contents.</p>"), output, ) self.assertEqual("Valid Content with Table of Contents", str(metadata["title"])) self.assertEqual("My Author", str(metadata["author"])) self.assertEqual("2020-10-16 00:00:00", str(metadata["date"])) self.assertEqual( '<nav class="toc" role="doc-toc">\n' "<ul>\n" '<li><a href="#first-heading">First Heading</a></li>\n' '<li><a href="#second-heading">Second Heading</a>\n' "<ul>\n" '<li><a href="#first-subheading">First Subheading</a></li>\n' '<li><a href="#second-subheading">Second Subheading</a></li>\n' "</ul></li>\n" "</ul>\n" "</nav>", str(metadata["toc"]), )
def test_invalid_self_contained_argument(self): """Check that specifying --self-contained raises an exception.""" pandoc_arguments = ["--self-contained"] settings = get_settings( PANDOC_EXTENSIONS=PANDOC_EXTENSIONS, PANDOC_ARGS=pandoc_arguments ) pandoc_reader = PandocReader(settings) source_path = os.path.join(TEST_CONTENT_PATH, "valid_content.md") with self.assertRaises(ValueError) as context_manager: pandoc_reader.read(source_path) message = str(context_manager.exception) self.assertEqual("Argument --self-contained is not supported.", message)
def test_metadata_block_end_with_leading_spaces(self): """Check if metadata block ending with leading spaces throws an exception.""" settings = get_settings(PANDOC_EXTENSIONS=PANDOC_EXTENSIONS, PANDOC_ARGS=PANDOC_ARGS) pandoc_reader = PandocReader(settings) source_path = os.path.join(TEST_CONTENT_PATH, "metadata_end_with_leading_spaces.md") # Metadata end --- or ... should not have leading spaces with self.assertRaises(Exception) as context_manager: pandoc_reader.read(source_path) message = str(context_manager.exception) self.assertEqual("Could not find end of metadata block.", message)
def test_user_defined_wpm_reading_time(self): """Check if 100 words per minute user defined gives us 2 minutes.""" settings = get_settings( PANDOC_EXTENSIONS=PANDOC_EXTENSIONS, PANDOC_ARGS=PANDOC_ARGS, CALCULATE_READING_TIME=CALCULATE_READING_TIME, READING_SPEED=100, ) pandoc_reader = PandocReader(settings) source_path = os.path.join(TEST_CONTENT_PATH, "reading_time_content.md") _, metadata = pandoc_reader.read(source_path) self.assertEqual("2 minutes", str(metadata["reading_time"]))
def test_pandoc_unsupported_minor_version(self): """Check if the installed pandoc has a supported minor version.""" settings = get_settings( PANDOC_EXTENSIONS=PANDOC_EXTENSIONS, PANDOC_ARGS=PANDOC_ARGS, PANDOC_EXECUTABLE_PATH="2.10/bin/pandoc", ) pandoc_reader = PandocReader(settings) source_path = os.path.join(TEST_CONTENT_PATH, "empty.md") with self.assertRaises(Exception) as context_manager: pandoc_reader.read(source_path) message = str(context_manager.exception) self.assertEqual("Pandoc version must be 2.11 or higher.", message)
def test_no_input_format(self): """Check if exception is raised if no input format is specified.""" pandoc_default_files = [ os.path.join(TEST_DEFAULT_FILES_PATH, "no_input_format.yaml") ] settings = get_settings(PANDOC_DEFAULT_FILES=pandoc_default_files) pandoc_reader = PandocReader(settings) source_path = os.path.join(TEST_CONTENT_PATH, "valid_content.md") with self.assertRaises(ValueError) as context_manager: pandoc_reader.read(source_path) message = str(context_manager.exception) self.assertEqual("No input format specified.", message)
def test_pandoc_availability_two(self): """Check if pandoc executable is available at the given path.""" settings = get_settings( PANDOC_EXTENSIONS=PANDOC_EXTENSIONS, PANDOC_ARGS=PANDOC_ARGS, PANDOC_EXECUTABLE_PATH="2.11/bin/pandoc", ) pandoc_reader = PandocReader(settings) source_path = os.path.join(TEST_CONTENT_PATH, "empty.md") with self.assertRaises(Exception) as context_manager: pandoc_reader.read(source_path) message = str(context_manager.exception) self.assertEqual("Could not find Pandoc. Please install.", message)
def test_invalid_self_contained(self): """Check if exception is raised if self-contained is true.""" pandoc_default_files = [ os.path.join(TEST_DEFAULT_FILES_PATH, "selfcontained_true.yaml") ] settings = get_settings(PANDOC_DEFAULT_FILES=pandoc_default_files) pandoc_reader = PandocReader(settings) source_path = os.path.join(TEST_CONTENT_PATH, "valid_content.md") with self.assertRaises(ValueError) as context_manager: pandoc_reader.read(source_path) message = str(context_manager.exception) self.assertEqual("The default self-contained should be set to false.", message)
def test_invalid_user_defined_wpm(self): """Check if exception is raised if words per minute is not a number.""" settings = get_settings( PANDOC_EXTENSIONS=PANDOC_EXTENSIONS, PANDOC_ARGS=PANDOC_ARGS, CALCULATE_READING_TIME=CALCULATE_READING_TIME, READING_SPEED="my words per minute", ) pandoc_reader = PandocReader(settings) source_path = os.path.join(TEST_CONTENT_PATH, "reading_time_content.md") with self.assertRaises(ValueError) as context_manager: pandoc_reader.read(source_path) message = str(context_manager.exception) self.assertEqual("READING_SPEED setting must be a number.", message)
def test_invalid_to_output_format(self): """Check if exception is raised if to output format is invalid.""" pandoc_default_files = [ os.path.join(TEST_DEFAULT_FILES_PATH, "invalid_to_output_format.yaml") ] settings = get_settings(PANDOC_DEFAULT_FILES=pandoc_default_files) pandoc_reader = PandocReader(settings) source_path = os.path.join(TEST_CONTENT_PATH, "valid_content.md") with self.assertRaises(ValueError) as context_manager: pandoc_reader.read(source_path) message = str(context_manager.exception) self.assertEqual("Output format type must be either html or html5.", message)
def test_pandoc_availability_one(self): """Check if Pandoc executable is available.""" settings = get_settings( PANDOC_EXTENSIONS=PANDOC_EXTENSIONS, PANDOC_ARGS=PANDOC_ARGS, ) pandoc_reader = PandocReader(settings) source_path = os.path.join(TEST_CONTENT_PATH, "empty.md") if not shutil.which("pandoc"): # Case where pandoc is not available with self.assertRaises(Exception) as context_manager: pandoc_reader.read(source_path) message = str(context_manager.exception) self.assertEqual("Could not find Pandoc. Please install.", message) else: self.assertTrue(True)
def test_valid_file(self): """Check if we get the appropriate output for valid input.""" settings = get_settings(PANDOC_EXTENSIONS=PANDOC_EXTENSIONS, PANDOC_ARGS=PANDOC_ARGS) pandoc_reader = PandocReader(settings) source_path = os.path.join(TEST_CONTENT_PATH, "valid_content.md") output, metadata = pandoc_reader.read(source_path) self.assertEqual( ("<p>This is some valid content that should pass." " If it does not pass we" " will know something is wrong.</p>"), output, ) self.assertEqual("Valid Content", str(metadata["title"])) self.assertEqual("My Author", str(metadata["author"])) self.assertEqual("2020-10-16 00:00:00", str(metadata["date"]))
def test_mathjax_content(self): """Check if mathematics is rendered correctly.""" settings = get_settings(PANDOC_EXTENSIONS=PANDOC_EXTENSIONS, PANDOC_ARGS=PANDOC_ARGS) pandoc_reader = PandocReader(settings) source_path = os.path.join(TEST_CONTENT_PATH, "mathjax_content.md") output, metadata = pandoc_reader.read(source_path) self.assertEqual( ('<p><span class="math display">\\[\n' "e^{i\\theta} = \\cos\\theta + i \\sin\\theta.\n" "\\]</span></p>"), output, ) self.assertEqual("MathJax Content", str(metadata["title"])) self.assertEqual("My Author", str(metadata["author"])) self.assertEqual("2020-10-16 00:00:00", str(metadata["date"]))
def test_summary(self): """Check if summary output is valid.""" settings = get_settings( PANDOC_EXTENSIONS=PANDOC_EXTENSIONS, PANDOC_ARGS=PANDOC_ARGS, FORMATTED_FIELDS=FORMATTED_FIELDS, ) pandoc_reader = PandocReader(settings) source_path = os.path.join(TEST_CONTENT_PATH, "valid_content_with_citation.md") _, metadata = pandoc_reader.read(source_path) self.assertEqual( ("But this foundational principle of science has now been" " called into question by" ' <a href="https://www.britannica.com/science/string-theory">' "String Theory</a>."), str(metadata["summary"]), )
def test_to_writer_both_given(self): """Check if exception is raised if to and writer are both given.""" pandoc_default_files = [ os.path.join(TEST_DEFAULT_FILES_PATH, "to_writer_both_given.yaml") ] settings = get_settings(PANDOC_DEFAULT_FILES=pandoc_default_files) pandoc_reader = PandocReader(settings) source_path = os.path.join(TEST_CONTENT_PATH, "valid_content.md") with self.assertRaises(ValueError) as context_manager: pandoc_reader.read(source_path) message = str(context_manager.exception) self.assertEqual( ("Specifying both to and writer is not supported." " Please specify just one."), message, )
def test_valid_file_with_valid_defaults(self): """Check if we get the appropriate output specifying defaults.""" pandoc_default_files = [ os.path.join(TEST_DEFAULT_FILES_PATH, "valid_defaults.yaml") ] settings = get_settings(PANDOC_DEFAULT_FILES=pandoc_default_files) pandoc_reader = PandocReader(settings) source_path = os.path.join(TEST_CONTENT_PATH, "valid_content.md") output, metadata = pandoc_reader.read(source_path) self.assertEqual( ( "<p>This is some valid content that should pass." " If it does not pass we will know something is wrong.</p>" ), output, ) self.assertEqual("Valid Content", str(metadata["title"])) self.assertEqual("My Author", str(metadata["author"])) self.assertEqual("2020-10-16 00:00:00", str(metadata["date"]))
def test_citations_and_with_citeproc_filter(self): """Check if output, citations are valid using citeproc filter.""" pandoc_default_files = [ os.path.join( TEST_DEFAULT_FILES_PATH, "valid_defaults_with_citeproc_filter.yaml", ) ] settings = get_settings( PANDOC_DEFAULT_FILES=pandoc_default_files, ) pandoc_reader = PandocReader(settings) source_path = os.path.join(TEST_CONTENT_PATH, "valid_content_with_citation.md") output, metadata = pandoc_reader.read(source_path) self.maxDiff = None # pylint: disable=invalid-name self.assertEqual( ( '<h2 id="string-theory">String Theory</h2>\n' "<p>But this foundational principle of science has" " now been called into question by" ' <a href="https://www.britannica.com/science/' 'string-theory">String Theory</a>,' " which is a relative newcomer to theoretical physics, but one" " that has captured the common imagination, judging by" " the popular explanations that abound on the Web" ' <span class="citation" data-cites="mann2019 wood2019' ' jones2020">[1]–[3]</span>.' " And whether string theory is or is not science, Popper" " notwithstanding, is an issue that is still up for debate" " <span" ' class="citation" data-cites="siegel2015 castelvecchi2016' ' alves2017 francis2019">[4]–[7]</span>.</p>\n' '<h1 class="unnumbered" id="bibliography">References</h1>\n' '<div class="references csl-bib-body" id="refs"' ' role="doc-bibliography">\n' '<div class="csl-entry" id="ref-mann2019"' ' role="doc-biblioentry">\n' '<div class="csl-left-margin">[1]' ' </div><div class="csl-right-inline">A. Mann,' " <span>“<span>What Is String Theory?</span>”</span>" " 20-Mar-2019. [Online]." ' Available: <a href="https://www.livescience.com/' '65033-what-is-string-theory.html">' "https://www.livescience.com/" "65033-what-is-string-theory.html</a>." " [Accessed: 12-Nov-2020]</div>\n" "</div>\n" '<div class="csl-entry" id="ref-wood2019"' ' role="doc-biblioentry">\n' '<div class="csl-left-margin">[2] </div>' '<div class="csl-right-inline">' "C. Wood, <span>“<span>What Is String Theory?</span>." " Reference article:" " A simplified explanation and brief history of string" " theory,”</span> 11-Jul-2019." ' [Online]. Available: <a href="https://www.space.com/' '17594-string-theory.html">' "https://www.space.com/17594-string-theory.html</a>." " [Accessed: 12-Nov-2020]</div>\n" "</div>\n" '<div class="csl-entry" id="ref-jones2020"' ' role="doc-biblioentry">\n' '<div class="csl-left-margin">[3]' ' </div><div class="csl-right-inline">' 'A. Z. Jones, <span>“<span class="nocase">The Basics of String' " Theory</span>,”</span> 02-Mar-2019. [Online]. Available:" ' <a href="https://www.thoughtco.com/' 'what-is-string-theory-2699363">' "https://www.thoughtco.com/what-is-string-theory-2699363</a>." " [Accessed: 12-Nov-2020]</div>\n" "</div>\n" '<div class="csl-entry" id="ref-siegel2015"' ' role="doc-biblioentry">\n' '<div class="csl-left-margin">[4]' ' </div><div class="csl-right-inline">' "E. Siegel, <span>“<span>Why String Theory Is Not A Scientific" " Theory</span>,”</span> 23-Dec-2015. [Online]. Available:" " <a" ' href="https://www.forbes.com/sites/' "startswithabang/2015/12/23/" 'why-string-theory-is-not-science/">https://www.forbes.com/' "sites/startswithabang/2015/12/23/" "why-string-theory-is-not-science/</a>." " [Accessed: 12-Nov-2020]</div>\n" "</div>\n" '<div class="csl-entry" id="ref-castelvecchi2016"' ' role="doc-biblioentry">\n' '<div class="csl-left-margin">[5]' ' </div><div class="csl-right-inline">' 'D. Castelvecchi, <span>“<span class="nocase">' "Feuding physicists turn" " to philosophy for help</span>. String theory is at the" " heart of a debate over the integrity of the scientific" " method itself,”</span> 05-Jan-2016. [Online]. Available:" ' <a href="https://www.nature.com/news/' 'feuding-physicists-turn-to-philosophy-for-help-1.19076">' "https://www.nature.com/news/" "feuding-physicists-turn-to-philosophy-for-help-1.19076</a>." " [Accessed: 12-Nov-2020]</div>\n" "</div>\n" '<div class="csl-entry" id="ref-alves2017"' ' role="doc-biblioentry">\n' '<div class="csl-left-margin">[6] </div>' '<div class="csl-right-inline">' 'R. A. Batista and J. Primack, <span>“<span class="nocase">' "Is String theory falsifiable?</span>. Can a theory that isn’t" " completely testable still be useful to physics?”</span>" " [Online]." ' Available: <a href="https://metafact.io/factchecks/' '30-is-string-theory-falsifiable">' "https://metafact.io/factchecks/" "30-is-string-theory-falsifiable</a>." " [Accessed: 12-Nov-2020]</div>\n" "</div>\n" '<div class="csl-entry" id="ref-francis2019"' ' role="doc-biblioentry">\n' '<div class="csl-left-margin">[7]' ' </div><div class="csl-right-inline">' 'M. R. Francis, <span>“<span class="nocase">Falsifiability and' " physics</span>. Can a theory that isn’t completely testable" " still be useful to physics?”</span> 23-Apr-2019." " [Online]. Available:" ' <a href="https://www.scientificamerican.com/' 'article/is-string-theory-science/">' "https://www.scientificamerican.com/article/is-" "string-theory-science/</a>. [Accessed: 12-Nov-2020]</div>\n" "</div>\n" "</div>" ), output, ) self.assertEqual("Valid Content With Citation", str(metadata["title"])) self.assertEqual("My Author", str(metadata["author"])) self.assertEqual("2020-10-16 00:00:00", str(metadata["date"]))