from pathlib import Path from typing import Set from docutils import nodes, writers from docutils.io import StringOutput from sphinx.builders import Builder from sphinx.util import logging from sphinx.util.docutils import SphinxTranslator from sphinx.util.osutil import ensuredir, os_path logger = logging.getLogger(__name__) class GemtextWriter(writers.Writer): def __init__(self, builder): super().__init__() self.builder = builder def translate(self): visitor = self.builder.create_translator(self.document, self.builder) self.document.walkabout(visitor) self.output = '\n'.join(visitor.body) + '\n' class GemtextTranslator(SphinxTranslator): def __init__(self, document: nodes.document, builder): super().__init__(document, builder) self.heading_level = 0 self.current_line = '' self.body = [] self.literal = False self.pending_links = [] def _finish_line(self): if self.current_line != '': self.body.append(self.current_line) self.current_line = '' def visit_document(self, node: nodes.document): self.heading_level = 1 self.current_line = '' self.body = [] self.literal = False self.pending_links = [] def depart_document(self, node: nodes.document): self._finish_line() def visit_section(self, node: nodes.section): self.heading_level += 1 if len(self.body) > 0 and self.body[-1] != '': self.body.append('') def depart_section(self, node: nodes.section): self.heading_level -= 1 def visit_title(self, node: nodes.title): pass def depart_title(self, node: nodes.title): self.current_line = '#' * (self.heading_level - 1) + ' ' + self.current_line self._finish_line() self.body.append('') def visit_Text(self, node: nodes.Text): text = node.astext() if not self.literal: text = text.replace('\n', ' ') self.current_line += text def depart_Text(self, node: nodes.Text): pass def visit_paragraph(self, node: nodes.paragraph): pass def depart_paragraph(self, node: nodes.paragraph): self._finish_line() self.body += self.pending_links self.pending_links = [] self.body.append('') def visit_reference(self, node: nodes.reference): pass def depart_reference(self, node: nodes.reference): if 'refuri' not in node.attributes: return if self.current_line.startswith('=>'): self._finish_line() self.body.append('=> {} {}'.format(node.attributes['refuri'], node.astext())) else: self.pending_links.append('=> {} {}'.format(node.attributes['refuri'], node.astext())) def visit_image(self, node: nodes.image): if self.current_line == '': self.body.append('=> {} {}'.format(node.attributes['uri'], node.attributes['alt'])) else: raise NotImplementedError('inline images') def depart_image(self, node: nodes.image): pass def visit_comment(self, node: nodes.comment): raise nodes.SkipNode def visit_strong(self, node: nodes.strong): self.current_line += '**' depart_strong = visit_strong def visit_emphasis(self, node: nodes.emphasis): self.current_line += '_' depart_emphasis = visit_emphasis def visit_target(self, node: nodes.target): raise nodes.SkipNode def visit_bullet_list(self, node: nodes.bullet_list): pass def depart_bullet_list(self, node: nodes.bullet_list): if self.body[-1] != '': self.body.append('') def visit_list_item(self, node: nodes.list_item): self.current_line += '* ' def depart_list_item(self, node: nodes.list_item): if self.body[-1] == '': self.body = self.body[:-1] if self.body[-1].startswith('=>') and self.body[-1].endswith(self.body[-2].replace('* ', '')): self.body = self.body[:-2] + [self.body[-1]] def visit_compound(self, node: nodes.compound): pass def depart_compound(self, node: nodes.compound): pass def visit_inline(self, node: nodes.inline): pass def depart_inline(self, node: nodes.inline): pass def visit_title_reference(self, node: nodes.title_reference): pass def depart_title_reference(self, node: nodes.title_reference): pass def visit_literal(self, node: nodes.literal): self.current_line += '`' depart_literal = visit_literal def visit_literal_block(self, node: nodes.literal_block): if self.body[-1] != '': self.body.append('') self.body.append('```') self.literal = True def depart_literal_block(self, node: nodes.literal_block): self._finish_line() self.body.append('```') self.body.append('') self.literal = False def visit_index(self, node: nodes.Node): pass def depart_index(self, node: nodes.Node): pass def visit_desc(self, node: nodes.Node): pass def depart_desc(self, node: nodes.Node): pass visit_term = visit_list_item def depart_term(self, node: nodes.Node): self._finish_line() def visit_definition(self, node: nodes.Node): pass def depart_definition(self, node: nodes.Node): pass visit_desc_signature = visit_term depart_desc_signature = depart_term def visit_desc_name(self, node: nodes.Node): pass def depart_desc_name(self, node: nodes.Node): pass def visit_desc_content(self, node: nodes.Node): pass def depart_desc_content(self, node: nodes.Node): pass def visit_glossary(self, node: nodes.Node): pass def depart_glossary(self, node: nodes.Node): pass def visit_definition_list(self, node: nodes.Node): pass def depart_definition_list(self, node: nodes.Node): pass def visit_definition_list_item(self, node: nodes.Node): pass def depart_definition_list_item(self, node: nodes.Node): pass def visit_todo_node(self, node: nodes.Node): pass def depart_todo_node(self, node: nodes.Node): pass def visit_note(self, node: nodes.Node): pass def depart_note(self, node: nodes.Node): pass def visit_math(self, node: nodes.Node): pass def depart_math(self, node: nodes.Node): pass class GemtextBuilder(Builder): name = 'gmi' format = 'gmi' out_suffix = '.gmi' default_translator_class = GemtextTranslator def get_outdated_docs(self): yield from self.env.found_docs # can't be fucked to implement this right yield 'genindex' def get_target_uri(self, docname: str, typ: str = None): return docname + '.gmi' def prepare_writing(self, docnames: Set[str]): self.writer = GemtextWriter(self) def write_doc(self, docname: str, doctree: nodes.document): destination = StringOutput(encoding='utf-8') self.writer.write(doctree, destination) out_name = Path(self.outdir, os_path(docname) + '.gmi') out_name.parent.mkdir(parents=True, exist_ok=True) with open(out_name, 'w', encoding='utf-8') as out_file: out_file.write(self.writer.output) def setup(app): app.add_builder(GemtextBuilder) return { 'version': '0.1', 'parallel_read_safe': True, 'parallel_write_safe': True, }