#! /usr/bin/env python3 # Copyright 2019, 2020 Steinar Knutsen # # Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the # European Commission - subsequent versions of the EUPL (the "Licence"); You may # not use this work except in compliance with the Licence. You may obtain a copy # of the Licence at: # # https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 # # Unless required by applicable law or agreed to in writing, software # distributed under the Licence is distributed on an "AS IS" basis, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # Licence for the specific language governing permissions and limitations under # the Licence. # Groff use inspired by and partly stolen from # https://github.com/ratfactor/tjr/blob/master/tjr import sys, getopt, shutil help_text = """itxt2groff [-h] [-l linelength] [-u] [-L] -h print this text -l linelength explicit line length instead of fetching from the terminal/environment -n do not emit a .ll directive; useful if intending to use e.g. "groff -T ps" -u make sure ASCII 0x2D, hyphen/minus, in the input is preserved in output when using -Tutf8 or similar devices. This avoids breaking code examples. Do note using this switch with -Tps will strip the character. -L adjust text to left margin Read plain text from stdin and generate troff according to the following rules: - One Level of indent is four spaces in input _| - Pre-formatted blocks always start and end with a line of 80 consecutive "-" characters _| - Forced line break is inserted by a ending a line with "_|" _| - Indent where (#spaces % 4) != 0 is considered an error Intended to be used in a pipeline with groff, e.g. itxt2_groff < file | groff -T utf8 -D utf8""" HEAD = ".nh\n.fam H\n" FOOTER = ".pl 0\n" MONO = "--------------------------------------------------------------------------------" INDENT = 4 SPACE = " " START_MONO = ".nf\n.ft CR\n" END_MONO = ".ft\n.fi\n" SHIFT_RIGHT = '.in +4\n' PADDING = '.sp 1\n' PARA_END = ".in -4\n" BREAK_MARKER = "_|" BREAK = ".br\n" quote_table = { '`': r'\`', "'": r'\(aq', '-': r"\-", '~': r'\(ti', '^': r'\(ha', '\\': r'\\' } def get_indent(line): indent = 0 for c in line: if c == SPACE: indent += 1 else: break if (indent % INDENT) != 0: raise Exception("Indent not multiplum of INDENT constant.") return indent >> 2 def cook(line): unwritten = i = 0 buffer = [] if len(line) > 0 and line[0] == '.': buffer.append(r"\[char46]") unwritten += 1 i += 1 while i < len(line): subst = quote_table.get(line[i]) if subst != None: if unwritten != i: buffer.append(line[unwritten:i]) unwritten = i + 1 buffer.append(subst) i += 1 if unwritten < len(line): buffer.append(line[unwritten:]) return "".join(buffer) def ismonomarker(line): return line.rstrip() == MONO def write_line(destination, line, in_mono, line_length): if ismonomarker(line): destination.write(cook("-" * (line_length - 1) + "\n")) return if in_mono: destination.write(cook(line)) else: if line.rstrip().endswith(BREAK_MARKER): stripped = line.strip()[:-len(BREAK_MARKER)] + "\n" destination.write(cook(stripped)) else: destination.write(cook(line.lstrip())) def main(source, destination, line_length, explicit_line_length, left_adjust): previndent = currindent = 0 in_mono = False previous_line_empty = False suppress_vertical_padding = False padstack = [] prevline = "" destination.write(HEAD) if explicit_line_length: destination.write(".ll " + str(line_length) + "\n") if left_adjust: destination.write(".ad l\n") for line in source: if in_mono: write_line(destination, line, in_mono, line_length) if ismonomarker(line): in_mono = False destination.write(END_MONO) suppress_vertical_padding = True prevline = line else: if len(line.strip()) == 0: previous_line_empty = True if ismonomarker(prevline): destination.write("\n") prevline = line continue elif ismonomarker(line): in_mono = True for p in padstack: destination.write(PARA_END) del padstack[:] if ismonomarker(prevline) or len(prevline.strip()) == 0: destination.write("\n") destination.write(START_MONO) previndent = 0 write_line(destination, line, in_mono, line_length) prevline = line continue currindent = get_indent(line) if previndent > currindent: i = 0 while i < (previndent - currindent): destination.write(PARA_END) padstack.pop() i += 1 # not really needed to check for vertical padding suppression # here, but do it for maintanability if previous_line_empty and not suppress_vertical_padding: destination.write(PADDING) elif currindent > previndent: for i in range(currindent - previndent): destination.write(SHIFT_RIGHT) padstack.append(SHIFT_RIGHT) if previous_line_empty and not suppress_vertical_padding: destination.write(PADDING) elif previous_line_empty and not suppress_vertical_padding: destination.write(PADDING) elif prevline.rstrip().endswith(BREAK_MARKER): destination.write(BREAK) previous_line_empty = False prevline = line previndent = currindent suppress_vertical_padding = False write_line(destination, line, in_mono, line_length) for p in padstack: destination.write(PARA_END) destination.write(FOOTER) if __name__ == "__main__": line_length = shutil.get_terminal_size()[0] explicit_line_length = True left_adjust = False options, arguments = getopt.gnu_getopt(sys.argv[1:], "hl:nuL") for name, value in options: if name == "-h": print(help_text) sys.exit(0) elif name == "-l": line_length = int(value) elif name == "-n": explicit_line_length = False elif name == "-u": quote_table['-'] = r"\[u002D]" elif name == "-L": left_adjust = True main(sys.stdin, sys.stdout, line_length, explicit_line_length=explicit_line_length, left_adjust=left_adjust)