def __init__(self, config_file, default_quick_setting, **kwargs):
     super().__init__(**kwargs)
     # Note: allow_no_value=True  allows for #comments in the ini file
     self.logger = logging.getLogger(f'{config.yanom_globals.app_name}.'
                                     f'{what_module_is_this()}.'
                                     f'{self.__class__.__name__}'
                                     )
     self.logger.setLevel(config.yanom_globals.logger_level)
     self._config_file = config_file
     self._default_quick_setting = default_quick_setting
     self._conversion_settings = ConversionSettings()
     self._conversion_settings.set_quick_setting('manual')
     self._validation_values = self._conversion_settings.validation_values
 def setUp(self):
     self.conversion_settings = ConversionSettings()
     self.conversion_settings.set_quick_setting('gfm')
     files_to_convert = [
         Path('not_existing.md'),
         Path('some_markdown-old-1.md'),
         Path('renaming source file failed'),
         Path('a_folder/test_md_file.md'),
     ]
     self.file_converter = MDToHTMLConverter(self.conversion_settings,
                                             files_to_convert)
     self.file_converter._metadata_processor = MetaDataProcessor(
         self.conversion_settings)
Example #3
0
def test_generate_conversion_settings_using_quick_settings_object(
        good_config_ini, tmp_path):
    Path(f'{str(tmp_path)}/config.ini').write_text(good_config_ini,
                                                   encoding="utf-8")

    cd = config_data.ConfigData(f"{str(tmp_path)}/config.ini",
                                'gfm',
                                allow_no_value=True)

    cs = ConversionSettings()
    cs.quick_set_commonmark_settings()
    cd.generate_conversion_settings_using_quick_settings_object(cs)

    assert cd['quick_settings']['quick_setting'] == 'commonmark'
Example #4
0
def test_conversion_settings_property_obj_confirm_obj_read(
        tmp_path, good_config_ini):
    Path(f'{str(tmp_path)}/config.ini').write_text(good_config_ini,
                                                   encoding="utf-8")

    cd = config_data.ConfigData(f"{str(tmp_path)}/config.ini",
                                'gfm',
                                allow_no_value=True)

    cs = ConversionSettings()

    cs.quick_set_multimarkdown_settings()

    cd.conversion_settings = cs

    assert cd.conversion_settings.export_format == 'multimarkdown'
    assert cd['export_formats']['export_format'] == 'multimarkdown'
Example #5
0
def test_update_content_with_new_paths_for_non_movable_attachments(tmp_path):
    Path(tmp_path,
         'some_folder/data/my_notebook/attachments').mkdir(parents=True)
    Path(tmp_path, 'some_folder/attachments').mkdir(parents=True)
    Path(tmp_path, 'some_folder/data/attachments').mkdir(parents=True)
    Path(tmp_path,
         'some_folder/data/my_other_notebook/attachments').mkdir(parents=True)
    Path(tmp_path, 'some_folder/data/my_notebook/note.md').touch()
    Path(tmp_path, 'some_folder/data/my_notebook/attachments/one.png').touch()
    Path(tmp_path, 'some_folder/data/attachments/two.csv').touch()
    Path(tmp_path, 'some_folder/three.png').touch()
    Path(tmp_path, 'some_folder/attachments/four.csv').touch()
    Path(tmp_path, 'some_folder/four.csv').touch()
    Path(tmp_path,
         'some_folder/data/my_other_notebook/attachments/five.pdf').touch()
    Path(tmp_path, 'some_folder/data/my_notebook/six.csv').touch()
    Path(tmp_path,
         'some_folder/data/my_notebook/attachments/eight.pdf').touch()
    Path(tmp_path, 'some_folder/data/my_notebook/nine.md').touch()
    Path(tmp_path, 'some_folder/data/my_notebook/attachments/ten.png').touch()
    Path(tmp_path,
         'some_folder/data/my_notebook/attachments/eleven.pdf').touch()
    Path(tmp_path,
         'some_folder/data/my_notebook/attachments/file twelve.pdf').touch()
    Path(tmp_path,
         'some_folder/data/my_notebook/attachments/file fourteen.png').touch()

    file_path = Path(tmp_path, 'some_folder/data/my_notebook/note.md')
    content = f'![copyable|600]({helper_functions.path_to_posix_str(tmp_path)}/some_folder/data/my_notebook/attachments/one.png)\n' \
              f'![non-existing|600]({helper_functions.path_to_posix_str(tmp_path)}/some_folder/two.png)\n' \
              f'![non-copyable|600]({helper_functions.path_to_posix_str(tmp_path)}/some_folder/three.png)\n' \
              f'![non-copyable|600](../../three.png)\n' \
              f'![non-existing|600](attachments/three.pdf)\n' \
              f'![copyable|600](attachments/eight.pdf)\n' \
              f'![copyable](../attachments/two.csv)\n' \
              f'![non-copyable](../../attachments/four.csv)\n' \
              f'![non-existing](../my_notebook/seven.csv)\n' \
              f'![copyable](../my_notebook/six.csv)\n' \
              f'![copyable](../my_other_notebook/attachments/five.pdf "test tool tip text")\n' \
              f'![note link](nine.md)\n' \
              f'[a web link](https://www.google.com)\n' \
              f'<img src="attachments/ten.png" />\n' \
              f'<a href="attachments/eleven.pdf">example-attachment.pdf</a>\n' \
              f'![copyable](attachments/file%20twelve.pdf)\n' \
              f'<a href="attachments/file%20thirteen.pdf">example-attachment.pdf</a>\n' \
              f'<img src="attachments/file%20fourteen.png" />\n' \
              f'<a href="https://www.google.com">google</a>)\n' \
              f'![]()\n' \
              f'<img src="" />\n' \
              f'<a href="">empty href link</a>'

    expected_content = f'![copyable|600]({helper_functions.path_to_posix_str(tmp_path)}/some_folder/data/my_notebook/attachments/one.png)\n' \
                       f'![non-existing|600]({helper_functions.path_to_posix_str(tmp_path)}/some_folder/two.png)\n' \
                       f'![non-copyable|600]({helper_functions.path_to_posix_str(tmp_path)}/some_folder/three.png)\n' \
                       f'![non-copyable|600]({helper_functions.path_to_posix_str(tmp_path)}/some_folder/three.png)\n' \
                       f'![non-existing|600](attachments/three.pdf)\n' \
                       f'![copyable|600](attachments/eight.pdf)\n' \
                       f'![copyable](../attachments/two.csv)\n' \
                       f'![non-copyable]({str(tmp_path.as_posix())}/some_folder/attachments/four.csv)\n' \
                       f'![non-existing](../my_notebook/seven.csv)\n' \
                       f'![copyable](../my_notebook/six.csv)\n' \
                       f'![copyable](../my_other_notebook/attachments/five.pdf "test tool tip text")\n' \
                       f'![note link](nine.md)\n' \
                       f'[a web link](https://www.google.com)\n' \
                       f'<img src="attachments/ten.png"/>\n' \
                       f'<a href="attachments/eleven.pdf">example-attachment.pdf</a>\n' \
                       f'![copyable](attachments/file%20twelve.pdf)\n' \
                       f'<a href="attachments/file%20thirteen.pdf">example-attachment.pdf</a>\n' \
                       f'<img src="attachments/file%20fourteen.png"/>\n' \
                       f'<a href="https://www.google.com">google</a>)\n' \
                       f'![]()\n' \
                       f'<img src=""/>\n' \
                       f'<a href="">empty href link</a>'

    conversion_settings = ConversionSettings()
    conversion_settings.source = Path(tmp_path, 'some_folder/data')
    conversion_settings.export_folder = Path(tmp_path, 'some_folder/export')
    conversion_settings.export_format = 'obsidian'
    conversion_settings.conversion_input = 'markdown'
    conversion_settings.make_absolute = True
    file_converter = HTMLToMDConverter(conversion_settings, 'files_to_convert')
    file_converter._file = file_path
    file_converter._files_to_convert = {
        Path(tmp_path, 'some_folder/data/my_notebook/nine.md')
    }
    attachment_links = get_attachment_paths(
        file_converter._conversion_settings.source_absolute_root,
        file_converter._conversion_settings.export_format,
        file_converter._file, file_converter._files_to_convert, content)
    result_content = update_content_with_new_paths(
        content,
        file_converter._file,
        attachment_links.non_copyable_relative,
        file_converter._conversion_settings.make_absolute,
        file_converter._conversion_settings.export_folder_absolute,
    )

    assert result_content == expected_content
def test_generate_set_of_attachment_paths_html_export_format(tmp_path):
    Path(tmp_path,
         'some_folder/data/my_notebook/attachments').mkdir(parents=True)
    Path(tmp_path, 'some_folder/attachments').mkdir(parents=True)
    Path(tmp_path, 'some_folder/data/attachments').mkdir(parents=True)
    Path(tmp_path,
         'some_folder/data/my_other_notebook/attachments').mkdir(parents=True)
    Path(tmp_path,
         'some_folder/data/my_other_notebook/attachments/five.pdf').touch()
    Path(tmp_path, 'some_folder/data/my_notebook/nine.md').touch()
    Path(tmp_path, 'some_folder/data/my_notebook/attachments/ten.png').touch()
    Path(tmp_path,
         'some_folder/data/my_notebook/attachments/eleven.pdf').touch()
    Path(tmp_path,
         'some_folder/data/my_notebook/attachments/file twelve.pdf').touch()
    Path(tmp_path,
         'some_folder/data/my_notebook/attachments/file fourteen.png').touch()

    file_path = Path(tmp_path, 'some_folder/data/my_notebook/note.md')
    content = f'![copyable](../my_other_notebook/attachments/five.pdf "test tool tip text")\n' \
              f'![note link](nine.md)\n' \
              f'[a web link](https://www.google.com "google")\n' \
              f'<img src="attachments/ten.png" />\n' \
              f'<a href="attachments/eleven.pdf">example-attachment.pdf</a>\n' \
              f'![copyable](attachments/file%20twelve.pdf)\n' \
              f'<a href="attachments/file%20thirteen.pdf">example-attachment.pdf</a>\n' \
              f'<img src="attachments/file%20fourteen.png" />'

    # expected_content = f'![copyable](../my_other_notebook/attachments/five.pdf "test tool tip text")\n' \
    #                    f'![note link](nine.md)\n' \
    #                    f'[a web link](https:\\www.google.com "google")\n' \
    #                    f'<img src="attachments/ten.png" />\n' \
    #                    f'<a href="attachments/eleven.pdf">example-attachment.pdf</a>\n' \
    #                    f'![copyable](attachments/file%20twelve.pdf)\n' \
    #                    f'<a href="attachments/file%20thirteen.pdf">example-attachment.pdf</a>\n' \
    #                    f'<img src="attachments/file%20fourteen.png" />'

    conversion_settings = ConversionSettings()
    conversion_settings.source = Path(tmp_path, 'some_folder/data')
    conversion_settings.export_folder = Path(tmp_path, 'some_folder/export')
    conversion_settings.export_format = 'html'
    file_converter = MDToHTMLConverter(conversion_settings, 'files_to_convert')
    file_converter._file = file_path
    file_converter._files_to_convert = {
        Path(tmp_path, 'some_folder/data/my_notebook/nine.md')
    }
    attachment_links = get_attachment_paths(
        file_converter._conversion_settings.source_absolute_root,
        file_converter._conversion_settings.conversion_input,
        file_converter._file, file_converter._files_to_convert, content)

    assert Path(tmp_path, 'some_folder/data/my_notebook/attachments/ten.png') \
           in attachment_links.copyable_absolute

    assert Path(tmp_path, 'some_folder/data/my_notebook/attachments/eleven.pdf') \
           in attachment_links.copyable_absolute

    assert Path(tmp_path, 'some_folder/data/my_notebook/attachments/file fourteen.png') \
           in attachment_links.copyable_absolute

    assert Path(tmp_path, 'some_folder/data/my_other_notebook/attachments/five.pdf') \
           in attachment_links.copyable_absolute

    assert Path(tmp_path, 'some_folder/data/my_notebook/attachments/file twelve.pdf') \
           in attachment_links.copyable_absolute

    assert len(attachment_links.copyable_absolute) == 5

    assert f'attachments/file thirteen.pdf' in attachment_links.non_existing

    assert len(attachment_links.non_existing) == 1
class TestMDToHTMLConverter(unittest.TestCase):
    def setUp(self):
        self.conversion_settings = ConversionSettings()
        self.conversion_settings.set_quick_setting('gfm')
        files_to_convert = [
            Path('not_existing.md'),
            Path('some_markdown-old-1.md'),
            Path('renaming source file failed'),
            Path('a_folder/test_md_file.md'),
        ]
        self.file_converter = MDToHTMLConverter(self.conversion_settings,
                                                files_to_convert)
        self.file_converter._metadata_processor = MetaDataProcessor(
            self.conversion_settings)

    def test_pre_process_obsidian_image_links_if_required(self):
        test_strings = [
            (
                'obsidian',
                '![|600](filepath/image.png)',
                '<img src="filepath/image.png" width="600" />',
                'obsidian link to gfm failed',
            ),
            (
                'obsidian',
                '![](filepath/image.png)',
                '![](filepath/image.png)',
                'markdown std link not left unchanged',
            ),
            (
                'obsidian',
                '![|some-text](filepath/image.png)',
                '<img alt="|some-text" src="filepath/image.png" />',
                'markdown std with pipe and text link not left unchanged',
            ),
            (
                'commonmark',
                '![](filepath/image.png)',
                '![](filepath/image.png)',
                'non obsidian input image incorrectly changed',
            ),
            (
                'obsidian',
                '![|600](filepath/image(23).png)',
                '<img src="filepath/image(23).png" width="600" />',
                'obsidian link to gfm failed',
            ),
        ]

        for test_set in test_strings:
            with self.subTest(
                    msg=f'Testing image link format {test_set[1]} conversion'):
                self.conversion_settings.markdown_conversion_input = test_set[
                    0]
                self.file_converter._pre_processed_content = test_set[1]
                self.file_converter.pre_process_obsidian_image_links_if_required(
                )
                self.assertEqual(test_set[2],
                                 self.file_converter._pre_processed_content,
                                 test_set[3])

    def test_parse_metadata_if_required(self):
        test_strings = [
            ('pandoc_markdown',
             '---\nexcerpt: tl;dr\nlayout: post\ntitle: TITLE\n---\n\n# Hello',
             '# Hello', 'pandoc_markdown pre processing for meta data failed'),
            ('gfm',
             '---\nexcerpt: tl;dr\nlayout: post\ntitle: TITLE\n---\n\n# Hello',
             '# Hello', 'pre processing for meta data failed'),
        ]
        for test_set in test_strings:
            with self.subTest(msg='testing meta data handling'):
                with TempDirectory() as d:
                    source_file = Path(d.path, 'some_markdown.md')
                    source_file.touch()
                    self.assertTrue(source_file.exists())
                    self.file_converter._file = source_file

                    self.conversion_settings.markdown_conversion_input = test_set[
                        0]
                    self.file_converter._file_content = test_set[1]
                    self.file_converter._conversion_settings.source = Path(
                        d.path)
                    self.file_converter._conversion_settings.export_folder = Path(
                        d.path)
                    self.file_converter.pre_process_content()
                    self.assertEqual(
                        test_set[2],
                        self.file_converter._pre_processed_content,
                        test_set[3])

    def test_update_note_links(self):
        self.file_converter._files_to_convert = [
            Path('not_existing.md'),
            Path('some_markdown-old-1.md'),
            Path('renaming source file failed'),
            Path('a_folder/test_md_file.md'),
            Path('a_folder/test_.md_file.md'),
        ]

        content = '<p><a href="a_folder/test_md_file.md">md file</a></p>' \
                  '<p><a href="a_folder/test_.md_file.md">md file  with dot md in name</a></p>' \
                  '<p><a href="a_folder/test_pdf_file.pdf">non md file file</a>' \
                  '</p><p><a href="https://a_folder/test_pdf_file.md">web md file</a></p>' \
                  '<p><a href="a_folder/not_in_convert_list.md">non md file file</a></p>'

        self.file_converter._output_extension = '.html'
        new_content = update_href_link_suffix_in_content(
            content, self.file_converter._output_extension,
            self.file_converter._files_to_convert)

        print(content)
        print(new_content)
        self.assertTrue(
            '<p><a href="a_folder/test_md_file.html">md file</a></p>'
            in new_content,
            'clean md file extension not changed correctly',
        )
        self.assertTrue(
            '<p><a href="a_folder/test_.md_file.html">md file  with dot md in name</a></p>'
            in new_content,
            ' md file with dot md in name  not changed correctly',
        )
        self.assertTrue(
            '<p><a href="a_folder/test_pdf_file.pdf">non md file file</a></p>'
            in new_content,
            'non md file extension changed incorrectly',
        )
        self.assertTrue(
            '</p><p><a href="https://a_folder/test_pdf_file.md">web md file</a></p>'
            in new_content,
            'internet md file extension changed incorrectly',
        )
        self.assertTrue(
            '<p><a href="a_folder/not_in_convert_list.md">non md file file</a></p>'
            in new_content,
            'file with md file extension not in to be changed list changed incorrectly',
        )

    def test_pre_process_content2_rename_existing_file_and_its_link_in_content(
            self):
        self.file_converter._file_content = '[existing_md](a-file.md)\n<a href="a-file.md">should change</a>'
        self.file_converter._metadata_schema = ['title']
        self.file_converter._file = Path('a-file.md')
        self.file_converter._conversion_settings.export_format = 'gfm'
        self.file_converter._conversion_settings.conversion_input = 'markdown'
        with TempDirectory() as d:
            self.file_converter._conversion_settings.source = Path(d.path)
            self.file_converter._conversion_settings.export_folder = Path(
                d.path)
            Path(d.path, 'a-file.md').touch()

            self.file_converter.pre_process_content()
            assert Path(d.path, 'a-file-old-1.md').exists()

            assert self.file_converter._pre_processed_content \
                   == '[existing_md](a-file-old-1.md)\n<a href="a-file-old-1.md">should change</a>'

    def test_post_process_content(self):
        self.file_converter._conversion_settings.markdown_conversion_input = 'gfm'
        self.file_converter._converted_content = '<head><title>-</title></head><p><a href="a_folder/test_md_file.md">md file</a></p>'
        self.file_converter._metadata_processor._metadata = {
            'title': 'My Title'
        }
        self.file_converter._conversion_settings.export_format = 'html'
        self.file_converter._output_extension = file_mover.get_file_suffix_for(
            self.file_converter._conversion_settings.export_format)
        self.file_converter._file = Path('a_folder/file.html')
        self.file_converter.post_process_content()
        self.assertEqual(
            '<head><title>My Title</title><meta title="My Title"/></head><p><a href="a_folder/test_md_file.html">md file</a></p>',
            self.file_converter._post_processed_content,
            'title and meta data inserted incorrectly',
        )

    def test_add_meta_data_if_required(self):
        self.file_converter._conversion_settings.markdown_conversion_input = 'gfm'
        self.file_converter._post_processed_content = '<head><title>-</title></head><p><a href="a_folder/test_md_file.md">md file</a></p>'
        self.file_converter._metadata_processor._metadata = {
            'title': 'My Title'
        }
        self.file_converter.add_meta_data_if_required()
        self.assertEqual(
            '<head><title>My Title</title><meta title="My Title"/></head><p><a href="a_folder/test_md_file.md">md file</a></p>',
            self.file_converter._post_processed_content,
            'title and meta data inserted incorrectly',
        )

        self.file_converter._conversion_settings.markdown_conversion_input = 'pandoc_markdown'
        self.file_converter._post_processed_content = '<head><title>-</title></head><p><a href="a_folder/test_md_file.md">md file</a></p>'
        self.file_converter._metadata_processor._metadata = {
            'title': 'My Title'
        }
        self.file_converter.add_meta_data_if_required()
        self.assertEqual(
            '<head><title>My Title</title><meta title="My Title"/></head><p><a href="a_folder/test_md_file.md">md file</a></p>',
            self.file_converter._post_processed_content,
            'title and meta data inserted incorrectly with markdown conversion input',
        )
Example #8
0
def main():
    def generate_file_list(file_extension, path_to_files: Path):
        if path_to_files.is_file():
            return [path_to_files]

        file_list_generator = path_to_files.rglob(f'*{file_extension}')
        file_list = {item for item in file_list_generator}
        return file_list

    conversion_settings = ConversionSettings()
    conversion_settings.quick_set_gfm_settings()
    conversion_settings.working_directory = Path('/Users/nimbus')
    conversion_settings.export_format = 'obsidian'
    # conversion_settings.export_format = 'gfm'
    # conversion_settings.export_format = 'multimarkdown'
    conversion_settings.conversion_input = 'nimbus'
    conversion_settings.split_tags = True
    conversion_settings.source = 'source'
    conversion_settings.export_folder = 'target'
    conversion_settings.attachment_folder_name = 'assets'
    conversion_settings.front_matter_format = 'yaml'
    # conversion_settings.front_matter_format = 'toml'
    # conversion_settings.front_matter_format = 'json'
    # conversion_settings.front_matter_format = 'text'
    # conversion_settings.front_matter_format = 'none'
    conversion_settings.tag_prefix = '#'

    conversion_settings.keep_nimbus_row_and_column_headers = False
    conversion_settings.embed_these_document_types = ['md', 'pdf']
    conversion_settings.embed_these_image_types = [
        'png', 'jpg', 'jpeg', 'gif', 'bmp', 'svg'
    ]
    conversion_settings.embed_these_audio_types = [
        'mp3', 'webm', 'wav', 'm4a', 'ogg', '3gp', 'flac'
    ]
    conversion_settings.embed_these_video_types = ['mp4', 'webm', 'ogv']

    conversion_settings.unrecognised_tag_format = 'html'
    # options html = as html tag, text or '' = string content of tag

    nimbus_zip_files = generate_file_list(
        'zip', conversion_settings.source_absolute_root)

    convert_nimbus_notes(conversion_settings, nimbus_zip_files)
class ConfigData(ConfigParser):
    """
    Read, verify, update and write config.ini files

    Attributes
    ----------
    logger : logging object
        used for program logging
    config_file : str
        name of the configuration file to be parsed and to use for storing config data
    default_quick_setting :  str
        setting to be used if the config.ini is found to be invalid and uses chooses to use the default
    conversion_settings :  Child of ConversionSettings, holds config data in a format to be used outside of the class
    validation_values :  dict
        holds the values required to validate a config file read from disk

    """

    def __init__(self, config_file, default_quick_setting, **kwargs):
        super().__init__(**kwargs)
        # Note: allow_no_value=True  allows for #comments in the ini file
        self.logger = logging.getLogger(f'{config.yanom_globals.app_name}.'
                                        f'{what_module_is_this()}.'
                                        f'{self.__class__.__name__}'
                                        )
        self.logger.setLevel(config.yanom_globals.logger_level)
        self._config_file = config_file
        self._default_quick_setting = default_quick_setting
        self._conversion_settings = ConversionSettings()
        self._conversion_settings.set_quick_setting('manual')
        self._validation_values = self._conversion_settings.validation_values

    def parse_config_file(self):
        self.read_config_file()
        valid_config = self.validate_config_file()
        if not valid_config:
            self.ask_user_to_choose_new_default_config_file()
        self.generate_conversion_settings_from_parsed_config_file_data()
        self.logger.info(f"Settings from config.ini are {self._conversion_settings}")

    def generate_conversion_settings_using_quick_settings_string(self, value):
        if value in self._conversion_settings.valid_quick_settings:
            self._conversion_settings.set_quick_setting(value)
            self._load_and_save_settings()
            return

        self.logger.error(f"Passed invalid value - {value} - is not a recognised quick setting string")
        raise ValueError(f"Conversion setting parameter must be a valid quick setting string "
                         f"{self._conversion_settings.valid_quick_settings} received '{value}'")

    def generate_conversion_settings_using_quick_settings_object(self, value):
        if isinstance(value, ConversionSettings):
            self._conversion_settings = value
            self._load_and_save_settings()
            return

        self.logger.error(f"Passed invalid value - {value}")
        raise TypeError(f"Conversion setting parameter must be a valid quick setting "
                        f"{self._conversion_settings.valid_quick_settings} string or a ConversionSettings object")

    def validate_config_file(self) -> bool:
        """
        Validate config data read from config.ini.  Errors in the data will trigger a prompt user
        asking to create a new default configuration or exit the program.

        """
        self.logger.debug("attempting to validate config file")

        try:
            self._validate_config()
            self.logger.debug("config file validated")
            return True

        except ValueError as e:
            self.logger.warning(f"Config file invalid \n{e}")
            return False

    def ask_user_to_choose_new_default_config_file(self):
        ask_what_to_do = InvalidConfigFileCommandLineInterface()
        what_to_do = ask_what_to_do.run_cli()
        if what_to_do == 'exit':
            sys.exit(0)
        self.logger.info("User chose to create a default file")
        self._conversion_settings.set_quick_setting(self._default_quick_setting)
        self._load_and_save_settings()

    def _validate_config(self):
        """
        Validate the current Config Data for any errors by comparing to a set of validation values.

        Errors will raise a ValueError

        """
        for section, keys in self._validation_values.items():
            if section not in self:
                raise ValueError(f'Missing section {section} in the config ini file')

            for key, values in keys.items():
                if key not in self[section]:
                    raise ValueError(f'Missing entry for {key} under section {section} in the config ini file')

                if values:
                    if self[section][key] not in values:
                        raise ValueError(f'Invalid value of "{self[section][key]}" for {key} under section {section} '
                                         f'in the config file')

    def generate_conversion_settings_from_parsed_config_file_data(self):
        """
        Transcribe the values in a ConversionSettings object into the ConfigParser format used by this class

        """
        self._conversion_settings.conversion_input = self['conversion_inputs']['conversion_input']
        self._conversion_settings.markdown_conversion_input = \
            self['markdown_conversion_inputs']['markdown_conversion_input']
        self._conversion_settings.quick_setting = self['quick_settings']['quick_setting']
        self._conversion_settings.export_format = self['export_formats']['export_format']
        self._conversion_settings.front_matter_format = self['meta_data_options']['front_matter_format']
        if self._conversion_settings.export_format == 'pandoc_markdown':
            self._conversion_settings.front_matter_format = 'yaml'
        self._conversion_settings.metadata_schema = self['meta_data_options']['metadata_schema']
        self._conversion_settings.tag_prefix = self['meta_data_options']['tag_prefix']
        self._conversion_settings.spaces_in_tags = self.getboolean('meta_data_options', 'spaces_in_tags')
        self._conversion_settings.split_tags = self.getboolean('meta_data_options', 'split_tags')
        self._conversion_settings.tag_prefix = self['meta_data_options']['tag_prefix']
        if self['meta_data_options']['metadata_time_format'] != '':
            self._conversion_settings.metadata_time_format = self['meta_data_options']['metadata_time_format']
        if self['meta_data_options']['file_created_text'] != '':
            self._conversion_settings.file_created_text = self['meta_data_options']['file_created_text']
        if self['meta_data_options']['file_modified_text'] != '':
            self._conversion_settings.file_modified_text = self['meta_data_options']['file_modified_text']
        self._conversion_settings.first_row_as_header = self.getboolean('table_options', 'first_row_as_header')
        self._conversion_settings.first_column_as_header = self.getboolean('table_options', 'first_column_as_header')
        self._conversion_settings.chart_image = self.getboolean('chart_options', 'chart_image')
        self._conversion_settings.chart_csv = self.getboolean('chart_options', 'chart_csv')
        self._conversion_settings.chart_data_table = self.getboolean('chart_options', 'chart_data_table')
        self._conversion_settings.source = self['file_options']['source']
        if self['file_options']['export_folder'] == '':
            self['file_options']['export_folder'] = 'notes'
        self._conversion_settings.export_folder = self['file_options']['export_folder']
        if self['file_options']['attachment_folder_name'] == '':
            self['file_options']['attachment_folder_name'] = 'attachments'
        self._conversion_settings.attachment_folder_name = self['file_options']['attachment_folder_name']
        self._conversion_settings._creation_time_in_exported_file_name = \
            self.getboolean('file_options', 'creation_time_in_exported_file_name')
        self._conversion_settings.allow_spaces_in_filenames = \
            self.getboolean('file_options', 'allow_spaces_in_filenames')
        self._conversion_settings.allow_unicode_in_filenames = \
            self.getboolean('file_options', 'allow_unicode_in_filenames')
        self._conversion_settings.allow_uppercase_in_filenames = \
            self.getboolean('file_options', 'allow_uppercase_in_filenames')
        self._conversion_settings.allow_non_alphanumeric_in_filenames = \
            self.getboolean('file_options', 'allow_non_alphanumeric_in_filenames')
        self._conversion_settings.filename_spaces_replaced_by = self['file_options']['filename_spaces_replaced_by']
        self._conversion_settings.max_file_or_directory_name_length = self['file_options']['max_file_or_directory_name_length']
        self._conversion_settings.orphans = self['file_options']['orphans']
        self._conversion_settings.make_absolute = self.getboolean('file_options', 'make_absolute')
        self._conversion_settings.embed_these_document_types = self['nimbus_options']['embed_these_document_types']
        self._conversion_settings.embed_these_image_types = self['nimbus_options']['embed_these_image_types']
        self._conversion_settings.embed_these_audio_types = self['nimbus_options']['embed_these_audio_types']
        self._conversion_settings.embed_these_video_types = self['nimbus_options']['embed_these_video_types']
        self._conversion_settings.keep_nimbus_row_and_column_headers = \
            self.getboolean('nimbus_options', 'keep_nimbus_row_and_column_headers')
        self._conversion_settings.unrecognised_tag_format = self['nimbus_options']['unrecognised_tag_format']

    def _write_config_file(self):
        ini_path = Path(self.conversion_settings.working_directory, 'data', self._config_file)
        try:
            with open(ini_path, 'w') as config_file:
                self.write(config_file)
                self.logger.info("Saving configuration file")
        except FileNotFoundError as e:
            if not Path(ini_path.parent).is_dir():
                message = f"Unable to save config.ini file '{ini_path.parent}' " \
                          f"is not a directory. {e.strerror}"
                self.logger.error(message)
                self.logger.error(helper_functions.log_traceback(e))
                if not config.yanom_globals.is_silent:
                    print(message)
            else:
                message = f"Unable to save config.ini file " \
                          f"- '{ini_path}' " \
                          f"- {e.strerror}"
                self.logger.error(message)
                self.logger.error(helper_functions.log_traceback(e))
                if not config.yanom_globals.is_silent:
                    print(message)
        except IOError as e:
            message = f"Unable to save config.ini file `{ini_path.parent}`.\n{e}"
            self.logger.error(message)
            self.logger.error(helper_functions.log_traceback(e))
            if not config.yanom_globals.is_silent:
                print(message)

    def read_config_file(self):
        """
        Read config file. If file is missing generate a new one using default quick setting.

        If config file is missing set the conversion_settings to the default quick setting values
        and use that to generate a config data set.

        """
        self.logger.debug('reading config file')
        path = Path(self.conversion_settings.working_directory, 'data', self._config_file)

        if path.is_file():
            self.read(path)
            self.logger.info(f'Data read from INI file is {self.__repr__()}')
        else:
            self.logger.warning(f'config.ini missing at {path}, generating new file and settings set to default.')
            if not config.yanom_globals.is_silent:
                print("config.ini missing, generating new file.")
            self.conversion_settings = self._default_quick_setting

    def _load_and_save_settings(self):
        """
        Read a dictionary of config data, formatted for config file generation and store the new config file.

        """
        self._wipe_current_config()
        self.read_dict(self._generate_conversion_dict())
        self.logger.info(f"Quick setting {self['quick_settings']['quick_setting']} loaded")
        self._write_config_file()

    def _wipe_current_config(self):
        """
        Wipe the current config sections.

        When using read_dict new sections are added to the end of the current config. so when written to file they
        are out of place compared to the validation data.  This method is here to save having to manually delete the
        ini file when coding changes to the conversion dict.  That means it is here to save me having to remember to
        delete the ini file :-)
        """
        for section, keys in self._validation_values.items():
            self.remove_section(section)
        pass

    def _generate_conversion_dict(self):
        """
        Generate a dictionary of the current conversion settings.

        Returns
        -------
        Dict
            Dictionary, formatted for config file creation, using values from a ConversionSettings object

        """
        # comments are treated as 'values' with no value (value is set to None) i.e. they are dict entries
        # where the key is the #comment string and the value is None
        return {
            'conversion_inputs': {
                f'    # Valid entries are {", ".join(self._conversion_settings.valid_conversion_inputs)}': None,
                '    #  nsx = synology Note Station Export file': None,
                '    #  html = simple html based notes pages, no complex CSS or javascript': None,
                '    #  markdown =  text files in markdown format': None,
                'conversion_input': self._conversion_settings.conversion_input
            },
            'markdown_conversion_inputs': {
                f'    # Valid entries are {", ".join(self._conversion_settings.valid_markdown_conversion_inputs)}': None,
                'markdown_conversion_input': self._conversion_settings.markdown_conversion_input
            },
            'quick_settings': {
                f'    # Valid entries are {", ".join(self._conversion_settings.valid_export_formats)}': None,
                '    # use manual to use the manual settings in the sections below': None,
                '    # NOTE if an option other than - manual - is used the rest of the ': None,
                '    # settings in this file will be set automatically': None,
                '    #': None,
                "quick_setting": self._conversion_settings.quick_setting,
                '    # ': None,
                '    # The following sections only apply if the above is set to manual': None,
                '    #  ': None
            },
            'export_formats': {
                f'    # Valid entries are {", ".join(self._conversion_settings.valid_export_formats)}': None,
                "export_format": self._conversion_settings.export_format
            },
            'meta_data_options': {
                '    # Note: front_matter_format sets the presence and type of the section with metadata ': None,
                '    # retrieved from the source': None,
                f'    # Valid entries are {", ".join(self._conversion_settings.valid_front_matter_formats)}': None,
                '    # no entry will result in no front matter section': None,
                'front_matter_format': self._conversion_settings.front_matter_format,
                '    # metadata schema is a comma separated list of metadata keys that you wish to ': None,
                '    # restrict the retrieved metadata keys. for example ': None,
                '    # title, tags    will return those two if they are found': None,
                '    # If left blank any meta data found will be used': None,
                '    # The useful available keys in an nsx file are title, ctime, mtime, tag': None,
                'metadata_schema': ",".join(self._conversion_settings.metadata_schema),
                '    # tag prefix is a character you wish to be added to the front of any tag values ': None,
                '    # retrieved from metadata.  NOTE Use this if using front matter format "text" ': None,
                '    # or use is your markdown system uses a prefix in a front matter section (most wil not use a prefix) ': None,
                'tag_prefix': self._conversion_settings.tag_prefix,
                '    # spaces_in_tags if True will maintain spaces in tag words, if False spaces are replaced by a dash -': None,
                'spaces_in_tags': self._conversion_settings.spaces_in_tags,
                '    # split tags will split grouped tags into individual tags if True': None,
                '    # "Tag1", "Tag1/Sub Tag2"  will become "Tag1", "Sub Tag2"': None,
                '    # grouped tags are only split where a "/" character is found': None,
                'split_tags': self._conversion_settings.split_tags,
                '    # Meta data time format USED FOR NSX ONLY - enter a valid strftime date and time format with ': None,
                '    # additional % signs to escape the first % sign': None,
                '    # 3 examples are %%Y-%%m-%%d %%H:%%M:%%S%%Z    %%Y-%%m-%%d %%H:%%M:%%S   %%Y%%m%%d%%H%%M': None,
                '    # for formats see https://strftime.org/': None,
                '    # If left blank will default to %%Y-%%m-%%d %%H:%%M:%%S%%Z': None,
                'metadata_time_format': self._conversion_settings.metadata_time_format.replace('%', '%%'),
                # have to escape % signs for configparser as single % tells it to treat as string formatting
                # is written to file as %% and when read config parser will strip the extra % so what is used is correct
                '    # Replacement names for nsx creation time (ctime) and modified date (mtime) ': None,
                '    # If left blank will default to created and updated': None,
                'file_created_text': self._conversion_settings.file_created_text,
                'file_modified_text': self._conversion_settings.file_modified_text,
            },
            'table_options': {
                '  #  These two table options apply to NSX files ONLY': None,
                'first_row_as_header': self._conversion_settings.first_row_as_header,
                'first_column_as_header': self._conversion_settings.first_column_as_header
            },
            'chart_options': {
                '  #  These three chart options apply to NSX files ONLY': None,
                'chart_image': self._conversion_settings.chart_image,
                'chart_csv': self._conversion_settings.chart_csv,
                'chart_data_table': self._conversion_settings.chart_data_table
            },
            'file_options': {
                'source': self._conversion_settings.source,
                'export_folder': self._conversion_settings.export_folder,
                'attachment_folder_name': self._conversion_settings.attachment_folder_name,
                '    # The following options apply to directory names, and currently only apply filenames in NSX conversions.': None,
                'allow_spaces_in_filenames': self._conversion_settings.allow_spaces_in_filenames,
                'filename_spaces_replaced_by': self._conversion_settings.filename_spaces_replaced_by,
                'allow_unicode_in_filenames': self._conversion_settings.allow_unicode_in_filenames,
                'allow_uppercase_in_filenames': self._conversion_settings.allow_uppercase_in_filenames,
                'allow_non_alphanumeric_in_filenames': self._conversion_settings.allow_non_alphanumeric_in_filenames,
                'creation_time_in_exported_file_name': self._conversion_settings.allow_uppercase_in_filenames,
                '    # If True creation time as `yyyymmddhhmm-` will be added as prefix to file name': None,
                'max_file_or_directory_name_length': self._conversion_settings.max_file_or_directory_name_length,
                '    # The following options apply to directory names, and currently only apply to html and markdown conversions.': None,
                'orphans': self._conversion_settings.orphans,
                '    # orphans are files that are not linked to any notes.  Valid Values are': None,
                '    # ignore - orphan files are left where they are and are not moved to an export folder.': None,
                '    # copy - orphan files are coppied to the export folder in the same relative locations as the source.': None,
                '    # orphan - orphan files are moved to a directory named orphan in the export folder.': None,
                'make_absolute': self._conversion_settings.make_absolute,
                '    # Links to files that are not in the path forwards of the source directory will be ': None,
                '    # changed to absolute links if set to True.  For example "../../someplace/some_file.pdf"': None,
                '    # becomes /root/path/to/someplace/some_file.pdf"': None,
                '    # False will leave these links unchanged as relative links': None,
            },
            'nimbus_options': {
                '    # The following options apply to nimbus notes conversions': None,
                'embed_these_document_types': ",".join(self._conversion_settings.embed_these_document_types),
                'embed_these_image_types': ",".join(self._conversion_settings.embed_these_image_types),
                'embed_these_audio_types': ",".join(self._conversion_settings.embed_these_audio_types),
                'embed_these_video_types': ",".join(self._conversion_settings.embed_these_video_types),
                'keep_nimbus_row_and_column_headers': self._conversion_settings.keep_nimbus_row_and_column_headers,
                '    # For unrecognised html tags use either html or plain text': None,
                '    # html = inline html in markdown and html in html files': None,
                '    # text = extract any text and display as plain text in markdown and html': None,
                'unrecognised_tag_format': self._conversion_settings.unrecognised_tag_format,

            },
        }

    @property
    def conversion_settings(self):
        return self._conversion_settings

    @conversion_settings.setter
    def conversion_settings(self, value):
        """
        Receive a conversion setting as a quick setting string or a ConversionSettings object,
        set the _conversion_setting, generate and save config ini file for the setting received.

        Parameters
        ----------
        value : str or ConversionSettings
            If str must be a valid quick_setting string value.

        """
        if type(value) is str:
            self.generate_conversion_settings_using_quick_settings_string(value)
            return

        self.generate_conversion_settings_using_quick_settings_object(value)

    def __str__(self):
        display_dict = str({section: dict(self[section]) for section in self.sections()})
        return str(f'{self.__class__.__name__}{display_dict}')

    def __repr__(self):
        display_dict = str({section: dict(self[section]) for section in self.sections()})
        return str(f'{self.__class__.__name__}{display_dict}')