gemini-to-html.py (3172B) - raw


      1 #!/usr/bin/env python
      2 # -*- coding: utf-8 -*-
      3 # Copyright (C) 2021 Oscar Benedito <oscar@oscarbenedito.com>
      4 # License: Affero General Public License version 3 or later
      5 
      6 # Convert text/gemini files to HTML.
      7 #
      8 # Usage:
      9 #
     10 #     ./gemini-to-html.py [file.gmi]
     11 #
     12 # If no file is specified, standard input is used.
     13 
     14 import sys
     15 import re
     16 
     17 
     18 css = """
     19 body { margin: 1em auto; max-width: 700px; line-height: 1.5; font-family: sans-serif; padding: 0 1em; }
     20 a { color: #02d; }
     21 a:hover { color: #309; }
     22 .a:before { content: "⇒ "; color: #888; }
     23 pre { background-color: #eee; padding: 1rem; overflow-x: auto; }
     24 blockquote { padding: 0 0 0 1.2rem; border-left: 3px solid; }
     25 """
     26 
     27 
     28 if len(sys.argv) == 1:
     29     data = sys.stdin.readlines()
     30 elif len(sys.argv) == 2:
     31     try:
     32         with open(sys.argv[1], 'r') as f:
     33             data = f.readlines()
     34     except IOError:
     35         sys.stderr.write('Error reading file {}.\n'.format(sys.argv[1]))
     36         sys.exit(1)
     37 else:
     38     sys.stderr.write('Usage: {} file.gmi.\n'.format(sys.argv[0]))
     39     sys.exit(1)
     40 
     41 
     42 print('<!DOCTYPE html>')
     43 print('<html>')
     44 print('<head>')
     45 print('<meta charset="utf-8"/>')
     46 print('<meta name="viewport" content="width=device-width, initial-scale=1"/>')
     47 if data[0][:1] == '#' and data[0][:2] != '##':
     48     print('<title>{}</title>'.format(data[0][1:].strip()))
     49 if css is not None:
     50     print('<style>{}</style>'.format(css))
     51 print('</head>')
     52 print('<body>')
     53 
     54 
     55 state = ''
     56 
     57 for line in data:
     58     line = line[:-1]
     59     if state == 'ul':
     60         if line[:2] == '* ':
     61             print('<li>{}</li>'.format(line[2:]))
     62             continue
     63         else:
     64             print('</ul>')
     65             state = ''
     66     elif state[:3] == 'pre':
     67         if line[:3] == '```':
     68             print('</code></pre>')
     69             state = ''
     70             continue
     71         else:
     72             if state == 'pre-first':
     73                 sys.stdout.write('{}'.format(line))
     74                 state = 'pre'
     75             else:
     76                 sys.stdout.write('\n{}'.format(line))
     77             continue
     78 
     79     if line[:2] == '=>':
     80         # re.sub
     81         m = re.match('^=>[ \t]*(\S+)(?:[ \t]+(.+))?', line)
     82         if m is None:
     83             sys.stderr.write('Incorrect syntax on line of type link:\n'
     84                              '    {}'.format(line))
     85             print('<p>{}</p>'.format(line))
     86             continue
     87         text = m.group(2) if m.group(2) is not None else m.group(1)
     88         print('<p class="a"><a href="{}">{}</a></p>'.format(m.group(1), text))
     89     elif line[:3] == '```':
     90         if len(line) > 3:
     91             sys.stdout.write('<pre aria-label="{}"><code>'.format(line[3:]))
     92         else:
     93             sys.stdout.write('<pre><code>')
     94         state = 'pre-first'
     95     elif line[:3] == '###':
     96         print('<h3>{}</h3>'.format(line[3:]))
     97     elif line[:2] == '##':
     98         print('<h2>{}</h2>'.format(line[2:]))
     99     elif line[:1] == '#':
    100         print('<h1>{}</h1>'.format(line[1:]))
    101     elif line[:2] == '* ':
    102         print('<ul>\n<li>{}</li>'.format(line[2:]))
    103         state = 'ul'
    104     elif line[:1] == '>':
    105         print('<blockquote>{}</blockquote>'.format(line[1:]))
    106     else:
    107         print('<p>{}</p>'.format(line))
    108 
    109 print('</body>\n</html>')