#!/usr/bin/python
import os
import re
import sys
import markdown
from dataclasses import dataclass, field


help_ = \
"""
usage: impress -t file -i file [-D key value] [-o file] [-nc|-r|-h|-m]

ARGUMENTS
    -D key value....Define a custom key/value pair for the document 
                    header used to populate the template file. User 
                    defined pairs are ultimate and are not overriden 
                    further.
    -t file.........Specify the mandatory template file.
    -i file.........Specify the mandatory input file.
    -o file.........Specify the optional output file, defaulting to
                    `<base>/<root>.out` where `base` is the parent
                    directory of the input file, and `root` is the
                    name of the input file stripped of the last 
                    extension.
    -nc.............Do not read the content section of the input file.
    -r..............Do not fail on roaming markers after template has
                    finished processing.
    -h..............Print this terse help page.
    -m..............Print the full manual.

AUTHOR
    (C) Maxim Kasyanenko, 2021. MIT License.
"""

manual = \
"""
SYNOPSIS
    impress - markdown article to HTML page converter.

    This tool is intended to be a pre-processor for the amalgam linking
    tool generating index pages. Impress reads a loosely formatted file
    and generates key/value pairs which are printed into a template
    file's contents via the marker syntax `@<key>@`.

INPUT COMPOSITION
    An input file shall define three (3) empty line delimited sections: 
    (1) the header, (2) the brief summary, and (3) the content.

    HEADER
        The header consists of colon (:) delimited key/value pairs
        whose fields are stripped prior to the printing process. The
        key/value pair is split only on the first colon instance, so
        the usage of colons in the value is permitted.

        Special key/value pairs, called automatic pairs, are added to
        the header during runtime. See TEMPLATING for more information.

        User-defined key/value pairs may be specified on the 
        command-line using the -D option. These pairs are ultimate,
        i.e, they override existing pairs and themselves cannot be
        overriden.

    BRIEF SUMMARY
        The brief summary may be a multi-line paragraph giving a brief
        overview of the document. This section is often used when
        generating condensed article index entries.

    CONTENT
        The remaining content in the file is markdown which will
        automatically be converted into HTML using the community
        markdown package.

    Here is a sample document:

    |   header_key_1: value_1
    |   header_key_2: value_2
    |   ...
    >
    |   Here is a brief summary
    |   which may span multiple
    |   lines!
    >
    |   # Introduction
    |
    |   The remaining content is markdown.
    |   ...

    where `>` denotes the empty line section separators.

TEMPLATING
    Templates may substitute header information via the `@<key>@`
    syntax, where `key` references a key in the header table described
    above.

    Special key/value pairs, called automatic pairs, are added
    to the header during runtime:

        input
            Path to the input file (think -i flag).

        output
            Path to the output file (think -o flag).

        brief
            Brief summary section content.

        content
            HTML converted markdown content.

USAGE
    impress -t file -i file [-o file] [-D key value] [-nc|-r|-h|-m]

ARGUMENTS
    -D key value
        Define a custom key/value pair for the document header used to 
        populate the template file. User defined pairs are ultimate and 
        are not overriden further.

    -t file
        Specify the mandatory template file.

    -i file
        Specify the mandatory input file.

    -o file
        Specify the optional output file, defaulting to 
        `<base>/<root>.out` where `base` is the parent directory of the 
        input file, and `root` is the name of the input file stripped of 
        the last extension.

    -nc
        Do not read the content section of the input file.

    -r
        Do not fail on roaming markers after template has finished 
        processing.

    -h
        Print this terse help page.

    -m
        Print the full manual.

AUTHOR
    (C) Maxim Kasyanenko, 2021. MIT License.
"""


def jointmessage(*items):
    return ' '.join(str(x) for x in items)


def perror(*items):
    sys.stderr.write(f'(ERROR) {jointmessage(*items)}\n')


def pwarn(*items):
    sys.stdout.write(f'(WARN) {jointmessage(*items)}\n')


def pinfo(*items):
    sys.stdout.write(f'(INFO) {jointmessage(*items)}\n')


@dataclass
class Args: # refer to the manual above
    f_template: str = None
    f_input: str = None
    f_output: str = None
    defines: dict = field(default_factory=dict)
    no_content: bool = False # ignore input content section
    fail_on_roam: bool = True # unsubstituted markers in template
    help_: bool = False
    manual: bool = False


def listpop(lst):
    return lst.pop(0) if lst else None


def argvpop():
    return listpop(sys.argv)


def parseargs():
    args = Args()

    # skip program name argument
    argvpop()

    while sys.argv:
        word = argvpop()
        if   word == '-nc':
            args.no_content = argvpop()
        elif word == '-t':
            args.f_template = argvpop()
        elif word == '-i':
            args.f_input = argvpop()
        elif word == '-o':
            args.f_output = argvpop()
        elif word == '-r':
            args.fail_on_roam = False
        elif word == '-h':
            args.help_ = True
        elif word == '-m':
            args.manual = True
        elif word == '-D':
            key = argvpop()
            value = argvpop()
            if key is not None:
                args.defines[key] = value

    return args


def main():
    # check for args
    a = parseargs()

    if a.help_:
        print(help_)
        return 0

    if a.manual:
        print(manual)
        return 0

    # f_template
    if not a.f_template:
        perror('template file must be specified.')
        perror('try: impress -h')
        return 1

    if not os.path.isfile(a.f_template):
        perror(f'template file "{a.f_template}" does not exist.')
        return 1

    # f_input
    if not a.f_input:
        perror('input file must be specified.')
        perror('try: impress -h')
        return 1

    if not os.path.isfile(a.f_input):
        perror(f'input file "{a.f_input}" does not exist.')
        return 1

    f_input_base, f_input_name = os.path.split(a.f_input)
    f_input_root, _ = os.path.splitext(f_input_name)

    # f_output
    if not a.f_output:
        a.f_output = os.path.join(f_input_base, f'{f_input_root}.out')

    f_output_base, _ = os.path.split(a.f_output)
    try:
        if f_output_base:
            os.makedirs(f_output_base, exist_ok=True)
    except OSError as e:
        perror('failed to create base directory:', e)
        return 1
    
    # load the template
    try:
        with open(a.f_template) as h_in:
            template = h_in.read()
    except OSError as e:
        perror('failed to read template:', e)
        return 1

    # load input
    try:
        with open(a.f_input) as h_in:
            # firstly load in header
            header = dict()
            while (line := h_in.readline()) != '\n':
                entry = [ x.strip() for x in line.split(':', 1) ]
                if len(entry) != 2:
                    pwarn(f'line "{line}" is not a header entry!')
                    continue
                key, value = entry
                header[key] = value

            # secondly, load in the `brief` description.
            brief_lines = []
            while (line := h_in.readline()) != '\n':
                brief_lines.append(line)
            brief = ' '.join(brief_lines)

            # lastly, load remaining content
            if a.no_content:
                content = ''
            else:
                content = markdown.markdown(h_in.read())

    except OSError as e:
        perror('failed to read input:', e)
        return 1

    # automatic pairs and user defines
    header['input'] = a.f_input
    header['output'] = a.f_output
    header['brief'] = brief
    header['content'] = content

    header.update(a.defines)

    # populate the template
    for key, value in header.items():
        template = template.replace(f'@{key}@', value)

    # rename for clarity
    render = template

    # check for roaming markers
    found_roaming = False
    for i, line in enumerate(render.split('\n')):
        n = i + 1
        for match in re.findall(r'@[^@]*@', line):
            pwarn(f'roaming marker, line {n}:', match)
            found_roaming = True

    if found_roaming and a.fail_on_roam:
        perror('failed on roaming markers: -r to disable')
        return 1

    # write render to output
    try:
        with open(a.f_output, 'w') as h_out:
            h_out.write(render)
    except OSError as e:
        perror('failed to write render:', e)
        return 1

    #
    return 0


if __name__ == '__main__':
    exit(main())

65d128f2782a12d4cd0b31bdd7c47a20e736b061
<mask token>


def jointmessage(*items):
    return ' '.join(str(x) for x in items)


def perror(*items):
    sys.stderr.write(f'(ERROR) {jointmessage(*items)}\n')


def pwarn(*items):
    sys.stdout.write(f'(WARN) {jointmessage(*items)}\n')


def pinfo(*items):
    sys.stdout.write(f'(INFO) {jointmessage(*items)}\n')


@dataclass
class Args:
    f_template: str = None
    f_input: str = None
    f_output: str = None
    defines: dict = field(default_factory=dict)
    no_content: bool = False
    fail_on_roam: bool = True
    help_: bool = False
    manual: bool = False


def listpop(lst):
    return lst.pop(0) if lst else None


<mask token>


def parseargs():
    args = Args()
    argvpop()
    while sys.argv:
        word = argvpop()
        if word == '-nc':
            args.no_content = argvpop()
        elif word == '-t':
            args.f_template = argvpop()
        elif word == '-i':
            args.f_input = argvpop()
        elif word == '-o':
            args.f_output = argvpop()
        elif word == '-r':
            args.fail_on_roam = False
        elif word == '-h':
            args.help_ = True
        elif word == '-m':
            args.manual = True
        elif word == '-D':
            key = argvpop()
            value = argvpop()
            if key is not None:
                args.defines[key] = value
    return args


def main():
    a = parseargs()
    if a.help_:
        print(help_)
        return 0
    if a.manual:
        print(manual)
        return 0
    if not a.f_template:
        perror('template file must be specified.')
        perror('try: impress -h')
        return 1
    if not os.path.isfile(a.f_template):
        perror(f'template file "{a.f_template}" does not exist.')
        return 1
    if not a.f_input:
        perror('input file must be specified.')
        perror('try: impress -h')
        return 1
    if not os.path.isfile(a.f_input):
        perror(f'input file "{a.f_input}" does not exist.')
        return 1
    f_input_base, f_input_name = os.path.split(a.f_input)
    f_input_root, _ = os.path.splitext(f_input_name)
    if not a.f_output:
        a.f_output = os.path.join(f_input_base, f'{f_input_root}.out')
    f_output_base, _ = os.path.split(a.f_output)
    try:
        if f_output_base:
            os.makedirs(f_output_base, exist_ok=True)
    except OSError as e:
        perror('failed to create base directory:', e)
        return 1
    try:
        with open(a.f_template) as h_in:
            template = h_in.read()
    except OSError as e:
        perror('failed to read template:', e)
        return 1
    try:
        with open(a.f_input) as h_in:
            header = dict()
            while (line := h_in.readline()) != '\n':
                entry = [x.strip() for x in line.split(':', 1)]
                if len(entry) != 2:
                    pwarn(f'line "{line}" is not a header entry!')
                    continue
                key, value = entry
                header[key] = value
            brief_lines = []
            while (line := h_in.readline()) != '\n':
                brief_lines.append(line)
            brief = ' '.join(brief_lines)
            if a.no_content:
                content = ''
            else:
                content = markdown.markdown(h_in.read())
    except OSError as e:
        perror('failed to read input:', e)
        return 1
    header['input'] = a.f_input
    header['output'] = a.f_output
    header['brief'] = brief
    header['content'] = content
    header.update(a.defines)
    for key, value in header.items():
        template = template.replace(f'@{key}@', value)
    render = template
    found_roaming = False
    for i, line in enumerate(render.split('\n')):
        n = i + 1
        for match in re.findall('@[^@]*@', line):
            pwarn(f'roaming marker, line {n}:', match)
            found_roaming = True
    if found_roaming and a.fail_on_roam:
        perror('failed on roaming markers: -r to disable')
        return 1
    try:
        with open(a.f_output, 'w') as h_out:
            h_out.write(render)
    except OSError as e:
        perror('failed to write render:', e)
        return 1
    return 0


<mask token>

<mask token>


def jointmessage(*items):
    return ' '.join(str(x) for x in items)


def perror(*items):
    sys.stderr.write(f'(ERROR) {jointmessage(*items)}\n')


def pwarn(*items):
    sys.stdout.write(f'(WARN) {jointmessage(*items)}\n')


def pinfo(*items):
    sys.stdout.write(f'(INFO) {jointmessage(*items)}\n')


@dataclass
class Args:
    f_template: str = None
    f_input: str = None
    f_output: str = None
    defines: dict = field(default_factory=dict)
    no_content: bool = False
    fail_on_roam: bool = True
    help_: bool = False
    manual: bool = False


def listpop(lst):
    return lst.pop(0) if lst else None


def argvpop():
    return listpop(sys.argv)


def parseargs():
    args = Args()
    argvpop()
    while sys.argv:
        word = argvpop()
        if word == '-nc':
            args.no_content = argvpop()
        elif word == '-t':
            args.f_template = argvpop()
        elif word == '-i':
            args.f_input = argvpop()
        elif word == '-o':
            args.f_output = argvpop()
        elif word == '-r':
            args.fail_on_roam = False
        elif word == '-h':
            args.help_ = True
        elif word == '-m':
            args.manual = True
        elif word == '-D':
            key = argvpop()
            value = argvpop()
            if key is not None:
                args.defines[key] = value
    return args


def main():
    a = parseargs()
    if a.help_:
        print(help_)
        return 0
    if a.manual:
        print(manual)
        return 0
    if not a.f_template:
        perror('template file must be specified.')
        perror('try: impress -h')
        return 1
    if not os.path.isfile(a.f_template):
        perror(f'template file "{a.f_template}" does not exist.')
        return 1
    if not a.f_input:
        perror('input file must be specified.')
        perror('try: impress -h')
        return 1
    if not os.path.isfile(a.f_input):
        perror(f'input file "{a.f_input}" does not exist.')
        return 1
    f_input_base, f_input_name = os.path.split(a.f_input)
    f_input_root, _ = os.path.splitext(f_input_name)
    if not a.f_output:
        a.f_output = os.path.join(f_input_base, f'{f_input_root}.out')
    f_output_base, _ = os.path.split(a.f_output)
    try:
        if f_output_base:
            os.makedirs(f_output_base, exist_ok=True)
    except OSError as e:
        perror('failed to create base directory:', e)
        return 1
    try:
        with open(a.f_template) as h_in:
            template = h_in.read()
    except OSError as e:
        perror('failed to read template:', e)
        return 1
    try:
        with open(a.f_input) as h_in:
            header = dict()
            while (line := h_in.readline()) != '\n':
                entry = [x.strip() for x in line.split(':', 1)]
                if len(entry) != 2:
                    pwarn(f'line "{line}" is not a header entry!')
                    continue
                key, value = entry
                header[key] = value
            brief_lines = []
            while (line := h_in.readline()) != '\n':
                brief_lines.append(line)
            brief = ' '.join(brief_lines)
            if a.no_content:
                content = ''
            else:
                content = markdown.markdown(h_in.read())
    except OSError as e:
        perror('failed to read input:', e)
        return 1
    header['input'] = a.f_input
    header['output'] = a.f_output
    header['brief'] = brief
    header['content'] = content
    header.update(a.defines)
    for key, value in header.items():
        template = template.replace(f'@{key}@', value)
    render = template
    found_roaming = False
    for i, line in enumerate(render.split('\n')):
        n = i + 1
        for match in re.findall('@[^@]*@', line):
            pwarn(f'roaming marker, line {n}:', match)
            found_roaming = True
    if found_roaming and a.fail_on_roam:
        perror('failed on roaming markers: -r to disable')
        return 1
    try:
        with open(a.f_output, 'w') as h_out:
            h_out.write(render)
    except OSError as e:
        perror('failed to write render:', e)
        return 1
    return 0


<mask token>

<mask token>
help_ = """
usage: impress -t file -i file [-D key value] [-o file] [-nc|-r|-h|-m]

ARGUMENTS
    -D key value....Define a custom key/value pair for the document 
                    header used to populate the template file. User 
                    defined pairs are ultimate and are not overriden 
                    further.
    -t file.........Specify the mandatory template file.
    -i file.........Specify the mandatory input file.
    -o file.........Specify the optional output file, defaulting to
                    `<base>/<root>.out` where `base` is the parent
                    directory of the input file, and `root` is the
                    name of the input file stripped of the last 
                    extension.
    -nc.............Do not read the content section of the input file.
    -r..............Do not fail on roaming markers after template has
                    finished processing.
    -h..............Print this terse help page.
    -m..............Print the full manual.

AUTHOR
    (C) Maxim Kasyanenko, 2021. MIT License.
"""
manual = """
SYNOPSIS
    impress - markdown article to HTML page converter.

    This tool is intended to be a pre-processor for the amalgam linking
    tool generating index pages. Impress reads a loosely formatted file
    and generates key/value pairs which are printed into a template
    file's contents via the marker syntax `@<key>@`.

INPUT COMPOSITION
    An input file shall define three (3) empty line delimited sections: 
    (1) the header, (2) the brief summary, and (3) the content.

    HEADER
        The header consists of colon (:) delimited key/value pairs
        whose fields are stripped prior to the printing process. The
        key/value pair is split only on the first colon instance, so
        the usage of colons in the value is permitted.

        Special key/value pairs, called automatic pairs, are added to
        the header during runtime. See TEMPLATING for more information.

        User-defined key/value pairs may be specified on the 
        command-line using the -D option. These pairs are ultimate,
        i.e, they override existing pairs and themselves cannot be
        overriden.

    BRIEF SUMMARY
        The brief summary may be a multi-line paragraph giving a brief
        overview of the document. This section is often used when
        generating condensed article index entries.

    CONTENT
        The remaining content in the file is markdown which will
        automatically be converted into HTML using the community
        markdown package.

    Here is a sample document:

    |   header_key_1: value_1
    |   header_key_2: value_2
    |   ...
    >
    |   Here is a brief summary
    |   which may span multiple
    |   lines!
    >
    |   # Introduction
    |
    |   The remaining content is markdown.
    |   ...

    where `>` denotes the empty line section separators.

TEMPLATING
    Templates may substitute header information via the `@<key>@`
    syntax, where `key` references a key in the header table described
    above.

    Special key/value pairs, called automatic pairs, are added
    to the header during runtime:

        input
            Path to the input file (think -i flag).

        output
            Path to the output file (think -o flag).

        brief
            Brief summary section content.

        content
            HTML converted markdown content.

USAGE
    impress -t file -i file [-o file] [-D key value] [-nc|-r|-h|-m]

ARGUMENTS
    -D key value
        Define a custom key/value pair for the document header used to 
        populate the template file. User defined pairs are ultimate and 
        are not overriden further.

    -t file
        Specify the mandatory template file.

    -i file
        Specify the mandatory input file.

    -o file
        Specify the optional output file, defaulting to 
        `<base>/<root>.out` where `base` is the parent directory of the 
        input file, and `root` is the name of the input file stripped of 
        the last extension.

    -nc
        Do not read the content section of the input file.

    -r
        Do not fail on roaming markers after template has finished 
        processing.

    -h
        Print this terse help page.

    -m
        Print the full manual.

AUTHOR
    (C) Maxim Kasyanenko, 2021. MIT License.
"""


def jointmessage(*items):
    return ' '.join(str(x) for x in items)


def perror(*items):
    sys.stderr.write(f'(ERROR) {jointmessage(*items)}\n')


def pwarn(*items):
    sys.stdout.write(f'(WARN) {jointmessage(*items)}\n')


def pinfo(*items):
    sys.stdout.write(f'(INFO) {jointmessage(*items)}\n')


@dataclass
class Args:
    f_template: str = None
    f_input: str = None
    f_output: str = None
    defines: dict = field(default_factory=dict)
    no_content: bool = False
    fail_on_roam: bool = True
    help_: bool = False
    manual: bool = False


def listpop(lst):
    return lst.pop(0) if lst else None


def argvpop():
    return listpop(sys.argv)


def parseargs():
    args = Args()
    argvpop()
    while sys.argv:
        word = argvpop()
        if word == '-nc':
            args.no_content = argvpop()
        elif word == '-t':
            args.f_template = argvpop()
        elif word == '-i':
            args.f_input = argvpop()
        elif word == '-o':
            args.f_output = argvpop()
        elif word == '-r':
            args.fail_on_roam = False
        elif word == '-h':
            args.help_ = True
        elif word == '-m':
            args.manual = True
        elif word == '-D':
            key = argvpop()
            value = argvpop()
            if key is not None:
                args.defines[key] = value
    return args


def main():
    a = parseargs()
    if a.help_:
        print(help_)
        return 0
    if a.manual:
        print(manual)
        return 0
    if not a.f_template:
        perror('template file must be specified.')
        perror('try: impress -h')
        return 1
    if not os.path.isfile(a.f_template):
        perror(f'template file "{a.f_template}" does not exist.')
        return 1
    if not a.f_input:
        perror('input file must be specified.')
        perror('try: impress -h')
        return 1
    if not os.path.isfile(a.f_input):
        perror(f'input file "{a.f_input}" does not exist.')
        return 1
    f_input_base, f_input_name = os.path.split(a.f_input)
    f_input_root, _ = os.path.splitext(f_input_name)
    if not a.f_output:
        a.f_output = os.path.join(f_input_base, f'{f_input_root}.out')
    f_output_base, _ = os.path.split(a.f_output)
    try:
        if f_output_base:
            os.makedirs(f_output_base, exist_ok=True)
    except OSError as e:
        perror('failed to create base directory:', e)
        return 1
    try:
        with open(a.f_template) as h_in:
            template = h_in.read()
    except OSError as e:
        perror('failed to read template:', e)
        return 1
    try:
        with open(a.f_input) as h_in:
            header = dict()
            while (line := h_in.readline()) != '\n':
                entry = [x.strip() for x in line.split(':', 1)]
                if len(entry) != 2:
                    pwarn(f'line "{line}" is not a header entry!')
                    continue
                key, value = entry
                header[key] = value
            brief_lines = []
            while (line := h_in.readline()) != '\n':
                brief_lines.append(line)
            brief = ' '.join(brief_lines)
            if a.no_content:
                content = ''
            else:
                content = markdown.markdown(h_in.read())
    except OSError as e:
        perror('failed to read input:', e)
        return 1
    header['input'] = a.f_input
    header['output'] = a.f_output
    header['brief'] = brief
    header['content'] = content
    header.update(a.defines)
    for key, value in header.items():
        template = template.replace(f'@{key}@', value)
    render = template
    found_roaming = False
    for i, line in enumerate(render.split('\n')):
        n = i + 1
        for match in re.findall('@[^@]*@', line):
            pwarn(f'roaming marker, line {n}:', match)
            found_roaming = True
    if found_roaming and a.fail_on_roam:
        perror('failed on roaming markers: -r to disable')
        return 1
    try:
        with open(a.f_output, 'w') as h_out:
            h_out.write(render)
    except OSError as e:
        perror('failed to write render:', e)
        return 1
    return 0


if __name__ == '__main__':
    exit(main())

import os
import re
import sys
import markdown
from dataclasses import dataclass, field
help_ = """
usage: impress -t file -i file [-D key value] [-o file] [-nc|-r|-h|-m]

ARGUMENTS
    -D key value....Define a custom key/value pair for the document 
                    header used to populate the template file. User 
                    defined pairs are ultimate and are not overriden 
                    further.
    -t file.........Specify the mandatory template file.
    -i file.........Specify the mandatory input file.
    -o file.........Specify the optional output file, defaulting to
                    `<base>/<root>.out` where `base` is the parent
                    directory of the input file, and `root` is the
                    name of the input file stripped of the last 
                    extension.
    -nc.............Do not read the content section of the input file.
    -r..............Do not fail on roaming markers after template has
                    finished processing.
    -h..............Print this terse help page.
    -m..............Print the full manual.

AUTHOR
    (C) Maxim Kasyanenko, 2021. MIT License.
"""
manual = """
SYNOPSIS
    impress - markdown article to HTML page converter.

    This tool is intended to be a pre-processor for the amalgam linking
    tool generating index pages. Impress reads a loosely formatted file
    and generates key/value pairs which are printed into a template
    file's contents via the marker syntax `@<key>@`.

INPUT COMPOSITION
    An input file shall define three (3) empty line delimited sections: 
    (1) the header, (2) the brief summary, and (3) the content.

    HEADER
        The header consists of colon (:) delimited key/value pairs
        whose fields are stripped prior to the printing process. The
        key/value pair is split only on the first colon instance, so
        the usage of colons in the value is permitted.

        Special key/value pairs, called automatic pairs, are added to
        the header during runtime. See TEMPLATING for more information.

        User-defined key/value pairs may be specified on the 
        command-line using the -D option. These pairs are ultimate,
        i.e, they override existing pairs and themselves cannot be
        overriden.

    BRIEF SUMMARY
        The brief summary may be a multi-line paragraph giving a brief
        overview of the document. This section is often used when
        generating condensed article index entries.

    CONTENT
        The remaining content in the file is markdown which will
        automatically be converted into HTML using the community
        markdown package.

    Here is a sample document:

    |   header_key_1: value_1
    |   header_key_2: value_2
    |   ...
    >
    |   Here is a brief summary
    |   which may span multiple
    |   lines!
    >
    |   # Introduction
    |
    |   The remaining content is markdown.
    |   ...

    where `>` denotes the empty line section separators.

TEMPLATING
    Templates may substitute header information via the `@<key>@`
    syntax, where `key` references a key in the header table described
    above.

    Special key/value pairs, called automatic pairs, are added
    to the header during runtime:

        input
            Path to the input file (think -i flag).

        output
            Path to the output file (think -o flag).

        brief
            Brief summary section content.

        content
            HTML converted markdown content.

USAGE
    impress -t file -i file [-o file] [-D key value] [-nc|-r|-h|-m]

ARGUMENTS
    -D key value
        Define a custom key/value pair for the document header used to 
        populate the template file. User defined pairs are ultimate and 
        are not overriden further.

    -t file
        Specify the mandatory template file.

    -i file
        Specify the mandatory input file.

    -o file
        Specify the optional output file, defaulting to 
        `<base>/<root>.out` where `base` is the parent directory of the 
        input file, and `root` is the name of the input file stripped of 
        the last extension.

    -nc
        Do not read the content section of the input file.

    -r
        Do not fail on roaming markers after template has finished 
        processing.

    -h
        Print this terse help page.

    -m
        Print the full manual.

AUTHOR
    (C) Maxim Kasyanenko, 2021. MIT License.
"""


def jointmessage(*items):
    return ' '.join(str(x) for x in items)


def perror(*items):
    sys.stderr.write(f'(ERROR) {jointmessage(*items)}\n')


def pwarn(*items):
    sys.stdout.write(f'(WARN) {jointmessage(*items)}\n')


def pinfo(*items):
    sys.stdout.write(f'(INFO) {jointmessage(*items)}\n')


@dataclass
class Args:
    f_template: str = None
    f_input: str = None
    f_output: str = None
    defines: dict = field(default_factory=dict)
    no_content: bool = False
    fail_on_roam: bool = True
    help_: bool = False
    manual: bool = False


def listpop(lst):
    return lst.pop(0) if lst else None


def argvpop():
    return listpop(sys.argv)


def parseargs():
    args = Args()
    argvpop()
    while sys.argv:
        word = argvpop()
        if word == '-nc':
            args.no_content = argvpop()
        elif word == '-t':
            args.f_template = argvpop()
        elif word == '-i':
            args.f_input = argvpop()
        elif word == '-o':
            args.f_output = argvpop()
        elif word == '-r':
            args.fail_on_roam = False
        elif word == '-h':
            args.help_ = True
        elif word == '-m':
            args.manual = True
        elif word == '-D':
            key = argvpop()
            value = argvpop()
            if key is not None:
                args.defines[key] = value
    return args


def main():
    a = parseargs()
    if a.help_:
        print(help_)
        return 0
    if a.manual:
        print(manual)
        return 0
    if not a.f_template:
        perror('template file must be specified.')
        perror('try: impress -h')
        return 1
    if not os.path.isfile(a.f_template):
        perror(f'template file "{a.f_template}" does not exist.')
        return 1
    if not a.f_input:
        perror('input file must be specified.')
        perror('try: impress -h')
        return 1
    if not os.path.isfile(a.f_input):
        perror(f'input file "{a.f_input}" does not exist.')
        return 1
    f_input_base, f_input_name = os.path.split(a.f_input)
    f_input_root, _ = os.path.splitext(f_input_name)
    if not a.f_output:
        a.f_output = os.path.join(f_input_base, f'{f_input_root}.out')
    f_output_base, _ = os.path.split(a.f_output)
    try:
        if f_output_base:
            os.makedirs(f_output_base, exist_ok=True)
    except OSError as e:
        perror('failed to create base directory:', e)
        return 1
    try:
        with open(a.f_template) as h_in:
            template = h_in.read()
    except OSError as e:
        perror('failed to read template:', e)
        return 1
    try:
        with open(a.f_input) as h_in:
            header = dict()
            while (line := h_in.readline()) != '\n':
                entry = [x.strip() for x in line.split(':', 1)]
                if len(entry) != 2:
                    pwarn(f'line "{line}" is not a header entry!')
                    continue
                key, value = entry
                header[key] = value
            brief_lines = []
            while (line := h_in.readline()) != '\n':
                brief_lines.append(line)
            brief = ' '.join(brief_lines)
            if a.no_content:
                content = ''
            else:
                content = markdown.markdown(h_in.read())
    except OSError as e:
        perror('failed to read input:', e)
        return 1
    header['input'] = a.f_input
    header['output'] = a.f_output
    header['brief'] = brief
    header['content'] = content
    header.update(a.defines)
    for key, value in header.items():
        template = template.replace(f'@{key}@', value)
    render = template
    found_roaming = False
    for i, line in enumerate(render.split('\n')):
        n = i + 1
        for match in re.findall('@[^@]*@', line):
            pwarn(f'roaming marker, line {n}:', match)
            found_roaming = True
    if found_roaming and a.fail_on_roam:
        perror('failed on roaming markers: -r to disable')
        return 1
    try:
        with open(a.f_output, 'w') as h_out:
            h_out.write(render)
    except OSError as e:
        perror('failed to write render:', e)
        return 1
    return 0


if __name__ == '__main__':
    exit(main())

#!/usr/bin/python
import os
import re
import sys
import markdown
from dataclasses import dataclass, field


help_ = \
"""
usage: impress -t file -i file [-D key value] [-o file] [-nc|-r|-h|-m]

ARGUMENTS
    -D key value....Define a custom key/value pair for the document 
                    header used to populate the template file. User 
                    defined pairs are ultimate and are not overriden 
                    further.
    -t file.........Specify the mandatory template file.
    -i file.........Specify the mandatory input file.
    -o file.........Specify the optional output file, defaulting to
                    `<base>/<root>.out` where `base` is the parent
                    directory of the input file, and `root` is the
                    name of the input file stripped of the last 
                    extension.
    -nc.............Do not read the content section of the input file.
    -r..............Do not fail on roaming markers after template has
                    finished processing.
    -h..............Print this terse help page.
    -m..............Print the full manual.

AUTHOR
    (C) Maxim Kasyanenko, 2021. MIT License.
"""

manual = \
"""
SYNOPSIS
    impress - markdown article to HTML page converter.

    This tool is intended to be a pre-processor for the amalgam linking
    tool generating index pages. Impress reads a loosely formatted file
    and generates key/value pairs which are printed into a template
    file's contents via the marker syntax `@<key>@`.

INPUT COMPOSITION
    An input file shall define three (3) empty line delimited sections: 
    (1) the header, (2) the brief summary, and (3) the content.

    HEADER
        The header consists of colon (:) delimited key/value pairs
        whose fields are stripped prior to the printing process. The
        key/value pair is split only on the first colon instance, so
        the usage of colons in the value is permitted.

        Special key/value pairs, called automatic pairs, are added to
        the header during runtime. See TEMPLATING for more information.

        User-defined key/value pairs may be specified on the 
        command-line using the -D option. These pairs are ultimate,
        i.e, they override existing pairs and themselves cannot be
        overriden.

    BRIEF SUMMARY
        The brief summary may be a multi-line paragraph giving a brief
        overview of the document. This section is often used when
        generating condensed article index entries.

    CONTENT
        The remaining content in the file is markdown which will
        automatically be converted into HTML using the community
        markdown package.

    Here is a sample document:

    |   header_key_1: value_1
    |   header_key_2: value_2
    |   ...
    >
    |   Here is a brief summary
    |   which may span multiple
    |   lines!
    >
    |   # Introduction
    |
    |   The remaining content is markdown.
    |   ...

    where `>` denotes the empty line section separators.

TEMPLATING
    Templates may substitute header information via the `@<key>@`
    syntax, where `key` references a key in the header table described
    above.

    Special key/value pairs, called automatic pairs, are added
    to the header during runtime:

        input
            Path to the input file (think -i flag).

        output
            Path to the output file (think -o flag).

        brief
            Brief summary section content.

        content
            HTML converted markdown content.

USAGE
    impress -t file -i file [-o file] [-D key value] [-nc|-r|-h|-m]

ARGUMENTS
    -D key value
        Define a custom key/value pair for the document header used to 
        populate the template file. User defined pairs are ultimate and 
        are not overriden further.

    -t file
        Specify the mandatory template file.

    -i file
        Specify the mandatory input file.

    -o file
        Specify the optional output file, defaulting to 
        `<base>/<root>.out` where `base` is the parent directory of the 
        input file, and `root` is the name of the input file stripped of 
        the last extension.

    -nc
        Do not read the content section of the input file.

    -r
        Do not fail on roaming markers after template has finished 
        processing.

    -h
        Print this terse help page.

    -m
        Print the full manual.

AUTHOR
    (C) Maxim Kasyanenko, 2021. MIT License.
"""


def jointmessage(*items):
    return ' '.join(str(x) for x in items)


def perror(*items):
    sys.stderr.write(f'(ERROR) {jointmessage(*items)}\n')


def pwarn(*items):
    sys.stdout.write(f'(WARN) {jointmessage(*items)}\n')


def pinfo(*items):
    sys.stdout.write(f'(INFO) {jointmessage(*items)}\n')


@dataclass
class Args: # refer to the manual above
    f_template: str = None
    f_input: str = None
    f_output: str = None
    defines: dict = field(default_factory=dict)
    no_content: bool = False # ignore input content section
    fail_on_roam: bool = True # unsubstituted markers in template
    help_: bool = False
    manual: bool = False


def listpop(lst):
    return lst.pop(0) if lst else None


def argvpop():
    return listpop(sys.argv)


def parseargs():
    args = Args()

    # skip program name argument
    argvpop()

    while sys.argv:
        word = argvpop()
        if   word == '-nc':
            args.no_content = argvpop()
        elif word == '-t':
            args.f_template = argvpop()
        elif word == '-i':
            args.f_input = argvpop()
        elif word == '-o':
            args.f_output = argvpop()
        elif word == '-r':
            args.fail_on_roam = False
        elif word == '-h':
            args.help_ = True
        elif word == '-m':
            args.manual = True
        elif word == '-D':
            key = argvpop()
            value = argvpop()
            if key is not None:
                args.defines[key] = value

    return args


def main():
    # check for args
    a = parseargs()

    if a.help_:
        print(help_)
        return 0

    if a.manual:
        print(manual)
        return 0

    # f_template
    if not a.f_template:
        perror('template file must be specified.')
        perror('try: impress -h')
        return 1

    if not os.path.isfile(a.f_template):
        perror(f'template file "{a.f_template}" does not exist.')
        return 1

    # f_input
    if not a.f_input:
        perror('input file must be specified.')
        perror('try: impress -h')
        return 1

    if not os.path.isfile(a.f_input):
        perror(f'input file "{a.f_input}" does not exist.')
        return 1

    f_input_base, f_input_name = os.path.split(a.f_input)
    f_input_root, _ = os.path.splitext(f_input_name)

    # f_output
    if not a.f_output:
        a.f_output = os.path.join(f_input_base, f'{f_input_root}.out')

    f_output_base, _ = os.path.split(a.f_output)
    try:
        if f_output_base:
            os.makedirs(f_output_base, exist_ok=True)
    except OSError as e:
        perror('failed to create base directory:', e)
        return 1
    
    # load the template
    try:
        with open(a.f_template) as h_in:
            template = h_in.read()
    except OSError as e:
        perror('failed to read template:', e)
        return 1

    # load input
    try:
        with open(a.f_input) as h_in:
            # firstly load in header
            header = dict()
            while (line := h_in.readline()) != '\n':
                entry = [ x.strip() for x in line.split(':', 1) ]
                if len(entry) != 2:
                    pwarn(f'line "{line}" is not a header entry!')
                    continue
                key, value = entry
                header[key] = value

            # secondly, load in the `brief` description.
            brief_lines = []
            while (line := h_in.readline()) != '\n':
                brief_lines.append(line)
            brief = ' '.join(brief_lines)

            # lastly, load remaining content
            if a.no_content:
                content = ''
            else:
                content = markdown.markdown(h_in.read())

    except OSError as e:
        perror('failed to read input:', e)
        return 1

    # automatic pairs and user defines
    header['input'] = a.f_input
    header['output'] = a.f_output
    header['brief'] = brief
    header['content'] = content

    header.update(a.defines)

    # populate the template
    for key, value in header.items():
        template = template.replace(f'@{key}@', value)

    # rename for clarity
    render = template

    # check for roaming markers
    found_roaming = False
    for i, line in enumerate(render.split('\n')):
        n = i + 1
        for match in re.findall(r'@[^@]*@', line):
            pwarn(f'roaming marker, line {n}:', match)
            found_roaming = True

    if found_roaming and a.fail_on_roam:
        perror('failed on roaming markers: -r to disable')
        return 1

    # write render to output
    try:
        with open(a.f_output, 'w') as h_out:
            h_out.write(render)
    except OSError as e:
        perror('failed to write render:', e)
        return 1

    #
    return 0


if __name__ == '__main__':
    exit(main())
