Skip to content

md_writer

trestle.core.markdown.md_writer ¤

Create formatted markdown files with optional yaml header.

Attributes¤

logger = logging.getLogger(__name__) module-attribute ¤

Classes¤

MDWriter ¤

Simple class to create markdown files.

Source code in trestle/core/markdown/md_writer.py
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
class MDWriter():
    """Simple class to create markdown files."""

    def __init__(self, file_path: pathlib.Path, header_comments_dict: Optional[Dict[str, str]] = None):
        """Initialize the class."""
        self._file_path = file_path
        self._lines = []
        self._indent_level = 0
        self._indent_size = 2
        self._yaml_header = None
        self._header_comments_dict = header_comments_dict

    def _current_indent_space(self):
        if self._indent_level <= 0:
            return ''
        return ' ' * (self._indent_level * self._indent_size)

    def _add_line_raw(self, line: str) -> None:
        out_line = '' if self._is_blank(line) else line
        self._lines.append(out_line)

    def _add_indent_level(self, delta: int) -> None:
        self._indent_level += delta

    def exists(self) -> bool:
        """Check if the file already exists."""
        return self._file_path.exists()

    def add_yaml_header(self, header: dict) -> None:
        """Add the yaml header."""
        self._yaml_header = header

    def set_indent_level(self, level: int) -> None:
        """Set the current indent level."""
        self._indent_level = level

    def set_indent_step_size(self, size: int) -> None:
        """Set the indent step size in spaces."""
        self._indent_size = size

    def _is_blank(self, line: str) -> bool:
        return line.strip() == ''

    def _prev_blank_line(self) -> bool:
        return len(self._lines) > 0 and self._is_blank(self._lines[-1])

    def new_line(self, line: str) -> None:
        """Add a line of text to the output."""
        # prevent double empty lines
        out_line = '' if self._is_blank(line) else self._current_indent_space() + line
        if self._prev_blank_line() and out_line == '':
            return
        self._add_line_raw(out_line)

    def new_paraline(self, line: str) -> None:
        """Add a paragraph and a line to output."""
        self.new_paragraph()
        self.new_line(line)

    def new_paragraph(self):
        """Start a new paragraph."""
        self.new_line('')

    def new_header(self, level: int, title: str, add_new_line_after_header: bool = True) -> None:
        """Add new header."""
        # headers might be separated by blank lines
        self.new_paragraph()
        self.new_line('#' * level + ' ' + title)
        if add_new_line_after_header:
            self.new_paragraph()

    def new_hr(self) -> None:
        """Add horizontal rule."""
        self.new_paragraph()
        self.new_line(const.SSP_MD_HRULE_LINE)
        self.new_paragraph()

    def new_list(self, list_: List[Any]) -> None:
        """Add a list to the markdown."""
        # in general this is a list of lists
        # if string just write it out
        if isinstance(list_, str):
            if self._is_blank(list_):
                self.new_paragraph()
            else:
                self.new_line('- ' + list_)
        # else it is a sublist so indent
        else:
            self._add_indent_level(1)
            self.new_paragraph()
            for item in list_:
                if self._indent_level <= 0:
                    self.new_paragraph()
                self.new_list(item)
            self._add_indent_level(-1)

    def new_table(self, table_list: List[List[str]], header: List[str]):
        """Add table to the markdown. All rows must be of equal length."""
        header_str = '| ' + ' | '.join(header) + ' |'
        sep_str = '|---' * len(header) + '|'
        self.new_line(header_str)
        self.new_line(sep_str)
        for row in table_list:
            row_str = '| ' + ' | '.join(row) + ' |'
            self.new_line(row_str)

    def _check_header(self) -> None:
        while len(self._lines) > 0 and self._lines[0] == '':
            self._lines = self._lines[1:]

    def write_out(self) -> None:
        """Write out the markdown file."""
        self._check_header()
        try:
            self._file_path.parent.mkdir(exist_ok=True, parents=True)
            with open(self._file_path, 'w', encoding=const.FILE_ENCODING) as f:
                # Make sure yaml header is written first
                if self._yaml_header:
                    f.write('---\n')
                    yaml = YAML()
                    yaml.indent(mapping=2, sequence=4, offset=2)
                    yaml.dump(self._yaml_header, f)
                    f.write('---\n\n')

                f.write('\n'.join(self._lines))
                # if last line has text it will need an extra \n at end
                if self._lines and self._lines[-1]:
                    f.write('\n')
            # insert helpful comments into the header happens after header is written out
            for tag, comment in as_dict(self._header_comments_dict).items():
                if tag in as_dict(self._yaml_header):
                    file_utils.insert_text_in_file(self._file_path, tag, comment)
        except IOError as e:
            logger.debug(f'md_writer error attempting to write out md file {self._file_path} {e}')
            raise TrestleError(f'Error attempting to write out md file {self._file_path} {e}')

    def get_lines(self) -> List[str]:
        """Return the current lines in the file."""
        return self._lines

    def get_text(self) -> str:
        """Get the text as currently written."""
        return '\n'.join(self._lines)

    def cull_headings(self, md_in: pathlib.Path, cull_list: List[str], strict_match: bool = False) -> None:
        """
        Cull headers from the lines of input markdown file with optional strict string match.

        Args:
            md_in: the path of the markdown file being edited
            cull_list: the list of strings in headers that are to be culled
            strict_match: whether to require an exact string match on header key or just a substring

        Returns None and creates new markdown at the path specified during MDWriter construction
        It is allowed to overwrite the original file
        """
        markdown_api = MarkdownAPI()
        header, content = markdown_api.processor.process_markdown(md_in)
        self._yaml_header = header
        self._lines = content.delete_nodes_text(cull_list, strict_match)
        self.write_out()
Functions¤
__init__(file_path, header_comments_dict=None) ¤

Initialize the class.

Source code in trestle/core/markdown/md_writer.py
34
35
36
37
38
39
40
41
def __init__(self, file_path: pathlib.Path, header_comments_dict: Optional[Dict[str, str]] = None):
    """Initialize the class."""
    self._file_path = file_path
    self._lines = []
    self._indent_level = 0
    self._indent_size = 2
    self._yaml_header = None
    self._header_comments_dict = header_comments_dict
add_yaml_header(header) ¤

Add the yaml header.

Source code in trestle/core/markdown/md_writer.py
59
60
61
def add_yaml_header(self, header: dict) -> None:
    """Add the yaml header."""
    self._yaml_header = header
cull_headings(md_in, cull_list, strict_match=False) ¤

Cull headers from the lines of input markdown file with optional strict string match.

Parameters:

Name Type Description Default
md_in Path

the path of the markdown file being edited

required
cull_list List[str]

the list of strings in headers that are to be culled

required
strict_match bool

whether to require an exact string match on header key or just a substring

False

Returns None and creates new markdown at the path specified during MDWriter construction It is allowed to overwrite the original file

Source code in trestle/core/markdown/md_writer.py
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
def cull_headings(self, md_in: pathlib.Path, cull_list: List[str], strict_match: bool = False) -> None:
    """
    Cull headers from the lines of input markdown file with optional strict string match.

    Args:
        md_in: the path of the markdown file being edited
        cull_list: the list of strings in headers that are to be culled
        strict_match: whether to require an exact string match on header key or just a substring

    Returns None and creates new markdown at the path specified during MDWriter construction
    It is allowed to overwrite the original file
    """
    markdown_api = MarkdownAPI()
    header, content = markdown_api.processor.process_markdown(md_in)
    self._yaml_header = header
    self._lines = content.delete_nodes_text(cull_list, strict_match)
    self.write_out()
exists() ¤

Check if the file already exists.

Source code in trestle/core/markdown/md_writer.py
55
56
57
def exists(self) -> bool:
    """Check if the file already exists."""
    return self._file_path.exists()
get_lines() ¤

Return the current lines in the file.

Source code in trestle/core/markdown/md_writer.py
167
168
169
def get_lines(self) -> List[str]:
    """Return the current lines in the file."""
    return self._lines
get_text() ¤

Get the text as currently written.

Source code in trestle/core/markdown/md_writer.py
171
172
173
def get_text(self) -> str:
    """Get the text as currently written."""
    return '\n'.join(self._lines)
new_header(level, title, add_new_line_after_header=True) ¤

Add new header.

Source code in trestle/core/markdown/md_writer.py
 94
 95
 96
 97
 98
 99
100
def new_header(self, level: int, title: str, add_new_line_after_header: bool = True) -> None:
    """Add new header."""
    # headers might be separated by blank lines
    self.new_paragraph()
    self.new_line('#' * level + ' ' + title)
    if add_new_line_after_header:
        self.new_paragraph()
new_hr() ¤

Add horizontal rule.

Source code in trestle/core/markdown/md_writer.py
102
103
104
105
106
def new_hr(self) -> None:
    """Add horizontal rule."""
    self.new_paragraph()
    self.new_line(const.SSP_MD_HRULE_LINE)
    self.new_paragraph()
new_line(line) ¤

Add a line of text to the output.

Source code in trestle/core/markdown/md_writer.py
77
78
79
80
81
82
83
def new_line(self, line: str) -> None:
    """Add a line of text to the output."""
    # prevent double empty lines
    out_line = '' if self._is_blank(line) else self._current_indent_space() + line
    if self._prev_blank_line() and out_line == '':
        return
    self._add_line_raw(out_line)
new_list(list_) ¤

Add a list to the markdown.

Source code in trestle/core/markdown/md_writer.py
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
def new_list(self, list_: List[Any]) -> None:
    """Add a list to the markdown."""
    # in general this is a list of lists
    # if string just write it out
    if isinstance(list_, str):
        if self._is_blank(list_):
            self.new_paragraph()
        else:
            self.new_line('- ' + list_)
    # else it is a sublist so indent
    else:
        self._add_indent_level(1)
        self.new_paragraph()
        for item in list_:
            if self._indent_level <= 0:
                self.new_paragraph()
            self.new_list(item)
        self._add_indent_level(-1)
new_paragraph() ¤

Start a new paragraph.

Source code in trestle/core/markdown/md_writer.py
90
91
92
def new_paragraph(self):
    """Start a new paragraph."""
    self.new_line('')
new_paraline(line) ¤

Add a paragraph and a line to output.

Source code in trestle/core/markdown/md_writer.py
85
86
87
88
def new_paraline(self, line: str) -> None:
    """Add a paragraph and a line to output."""
    self.new_paragraph()
    self.new_line(line)
new_table(table_list, header) ¤

Add table to the markdown. All rows must be of equal length.

Source code in trestle/core/markdown/md_writer.py
127
128
129
130
131
132
133
134
135
def new_table(self, table_list: List[List[str]], header: List[str]):
    """Add table to the markdown. All rows must be of equal length."""
    header_str = '| ' + ' | '.join(header) + ' |'
    sep_str = '|---' * len(header) + '|'
    self.new_line(header_str)
    self.new_line(sep_str)
    for row in table_list:
        row_str = '| ' + ' | '.join(row) + ' |'
        self.new_line(row_str)
set_indent_level(level) ¤

Set the current indent level.

Source code in trestle/core/markdown/md_writer.py
63
64
65
def set_indent_level(self, level: int) -> None:
    """Set the current indent level."""
    self._indent_level = level
set_indent_step_size(size) ¤

Set the indent step size in spaces.

Source code in trestle/core/markdown/md_writer.py
67
68
69
def set_indent_step_size(self, size: int) -> None:
    """Set the indent step size in spaces."""
    self._indent_size = size
write_out() ¤

Write out the markdown file.

Source code in trestle/core/markdown/md_writer.py
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
def write_out(self) -> None:
    """Write out the markdown file."""
    self._check_header()
    try:
        self._file_path.parent.mkdir(exist_ok=True, parents=True)
        with open(self._file_path, 'w', encoding=const.FILE_ENCODING) as f:
            # Make sure yaml header is written first
            if self._yaml_header:
                f.write('---\n')
                yaml = YAML()
                yaml.indent(mapping=2, sequence=4, offset=2)
                yaml.dump(self._yaml_header, f)
                f.write('---\n\n')

            f.write('\n'.join(self._lines))
            # if last line has text it will need an extra \n at end
            if self._lines and self._lines[-1]:
                f.write('\n')
        # insert helpful comments into the header happens after header is written out
        for tag, comment in as_dict(self._header_comments_dict).items():
            if tag in as_dict(self._yaml_header):
                file_utils.insert_text_in_file(self._file_path, tag, comment)
    except IOError as e:
        logger.debug(f'md_writer error attempting to write out md file {self._file_path} {e}')
        raise TrestleError(f'Error attempting to write out md file {self._file_path} {e}')

Functions¤

handler: python