#!/usr/bin/env python3 from __future__ import print_function, unicode_literals import sys import os import io import argparse from string import Template NEWDOC_VERSION = "1.3.2" # Record whether we're running under Python 2 or 3 PYVERSION = sys.version_info.major # The configparser module is called ConfigParser in Python2 if PYVERSION == 2: import ConfigParser as cp else: import configparser as cp # Force Python 2 to use the UTF-8 encoding. Otherwise, loading a template # containing Unicode characters fails. if PYVERSION == 2: reload(sys) sys.setdefaultencoding('utf8') # The directory where the script is located SCRIPT_HOME_DIR = os.path.dirname(__file__) # The directory where templates are located, relative to this script TEMPLATES_DIR = os.path.join(SCRIPT_HOME_DIR, "templates") DEFAULT_OPTIONS = { "id_case": "lowercase", "word_separator": "-", # The names of template files for different doc types "assembly_template": os.path.join(TEMPLATES_DIR, "assembly_title.adoc"), "concept_template": os.path.join(TEMPLATES_DIR, "con_title.adoc"), "procedure_template": os.path.join(TEMPLATES_DIR, "proc_title.adoc"), "reference_template": os.path.join(TEMPLATES_DIR, "ref_title.adoc"), # Templates can be downloaded from a repository "online_templates": False } # def get_config_dir() -> str: def get_config_dir(): """ Finds the appropriate user configuration directory where newdoc can store its configuration. Extracted form the appdirs library: https://github.com/ActiveState/appdirs """ # Typical user config directories are: # Mac OS X: ~/Library/Preferences/<AppName> # Unix: ~/.config/<AppName> # or in $XDG_CONFIG_HOME, if defined # Win *: same as user_data_dir platform = sys.platform if platform == "linux": os_config_dir = os.getenv("XDG_CONFIG_HOME", os.path.expanduser("~/.config")) elif platform == "darwin": os_config_dir = os.path.expanduser("~/Library/Preferences/") elif platform == "win32": print("newdoc has not been tested on Windows, and the configuration " "file functionality is not available here.") os_config_dir = None else: os_config_dir = os.path.expanduser("~/.config") return os.path.join(os_config_dir, "newdoc") # def get_config() -> dict: def get_config(): """ Tries to find config options in the platform-specific user config file, otherwise falls back to defaults. """ config_dir = get_config_dir() config_file = os.path.join(config_dir, "newdoc.ini") # Copy default options to start with: options = DEFAULT_OPTIONS # Search for matching keys in the config file; if found, # update the options dict with them if os.path.isfile(config_file): config = cp.ConfigParser() try: config.read(config_file) except cp.MissingSectionHeaderError: print("Error: The [newdoc] section is required in the configuration file.") exit(1) for k in options.keys(): # The configparser library is different in Python 2 and 3. # This si the only 2/3-compatible way I've found for optional keys. try: options[k] = config.get("newdoc", k) except cp.NoOptionError: pass return options # def convert_title_to_id(title: str, doc_type: str) -> str: def convert_title_to_id(title, doc_type, options): """ Converts the human-readable title to an ID string. """ # Some substitution rules, such as capitalization and word separator, # are found in the `options` dict inherited from the calling scope if options["id_case"] in ["lowercase", "lower-case", "lower case"]: # Convert to lowercase: converted_id = title.lower() elif options["id_case"] in ["capitalize", "capitalise"]: # First letter capitalized, subsequent letters lower-case: converted_id = title.capitalize() elif options["id_case"] in ["preserve", "original"]: converted_id = title else: print("Error: ID capitalization option not recognized: '{}'".format( options["id_case"])) exit(1) # This dict specifies all char substitutions to make on the ID subst_map = { " ": options["word_separator"], "(": "", ")": "", "?": "", "!": "", "'": "", '"': "", "#": "", "%": "", "&": "", "*": "", ",": "", ".": "-", "/": "-", ":": "-", ";": "", "@": "-at-", "\\": "", "`": "", "$": "", "^": "", "|": "", # Remove known semantic markup from the ID: "[package]": "", "[option]": "", "[parameter]": "", "[variable]": "", "[command]": "", "[replaceable]": "", "[filename]": "", "[literal]": "", "[systemitem]": "", "[application]": "", "[function]": "", "[gui]": "", # Remove square brackets only after semantic markup: "[": "", "]": "", # TODO: Curly braces shouldn't appear in the title in the first place. # They'd be interpreted as attributes there. # Print an error in that case? Escape them with AciiDoc escapes? "{": "", "}": "", } # Perform the substitutions specified by the above dict/table for k in subst_map.keys(): v = subst_map[k] converted_id = converted_id.replace(k, v) # Make sure the converted ID doesn't contain double dashes ("--"), because # that breaks references to the ID while "--" in converted_id: converted_id = converted_id.replace("--", "-") return converted_id # def strip_comments(adoc_text: str) -> str: def strip_comments(adoc_text): """ This function accepts AsciiDoc source and returns a copy of it that is stripped of all line starting with "//". """ # Split the text into lines and select only those that don't start # with "//" lines = adoc_text.splitlines() no_comments = [l for l in lines if not l.startswith("//")] # Connect the lines again, deleting empty leading lines return "\n".join(no_comments).lstrip() # def write_file(converted_id: str, module_content: str) -> None: def write_file(out_file, module_content): """ This function writes the generated content into the appropriate file, performing necessary checks """ # In Python 2, the `input` function is called `raw_input` if PYVERSION == 2: compatible_input = raw_input else: compatible_input = input # Check if the file exists; abort if so if os.path.exists(out_file): print('File already exists: "{}"'.format(out_file)) # Ask the user how to proceed, loop after an unexpected answer decision = None while not decision: response = compatible_input("Overwrite it? [yes/no] ").lower() if response in ["yes", "y"]: print("Overwriting.") break elif response in ["no", "n"]: print("Preserving the older file.") exit(1) else: pass # Write the file with open(out_file, "w") as f: f.write(module_content) # In Python 2, the UTF-8 encoding has to be specified explicitly if PYVERSION == 2: with io.open(out_file, mode="w", encoding="utf-8") as f: f.write(module_content) else: with open(out_file, "w") as f: f.write(module_content) print("File successfully generated.") print("To include this file from an assembly, use:") print("include::<path>/{}[leveloffset=+1]".format(out_file)) # def create_module(title: str, doc_type: str, delete_comments: bool) -> None: def create_module(title, doc_type, options, delete_comments): """ The main function of the script that integrates the other functions """ # Convert the title to ID converted_id = convert_title_to_id(title, doc_type, options) # Derive a file name from the ID and the doc type prefixes = { "assembly": "assembly_", "concept": "con_", "procedure": "proc_", "reference": "ref_" } filename = prefixes[doc_type] + converted_id + ".adoc" # Read the content of the template template_file = os.path.expanduser(options[doc_type + "_template"]) # Make sure the template file exists if not os.path.isfile(template_file): print("Error: Template file not found: '{}'".format(template_file)) exit(1) # In Python 2, the UTF-8 encoding has to be specified explicitly if PYVERSION == 2: with io.open(template_file, mode="r", encoding="utf-8") as f: template = f.read() else: with open(template_file, "r") as f: template = f.read() # Prepare the content of the new module module_content = Template(template).substitute(module_title=title, module_id=converted_id, filename=filename) # If the --no-comments option is selected, delete all comments if delete_comments: module_content = strip_comments(module_content) # Write the generated content into a file write_file(filename, module_content) def main(): """ Main, executable procedure of the script """ # Build a command-line options parser parser = argparse.ArgumentParser() parser.add_argument("--version", action="version", version="newdoc {}".format(NEWDOC_VERSION)) parser.add_argument("-a", "--assembly", help="Create an assembly from a given title.", metavar="title", nargs="+", type=str) parser.add_argument("-c", "--concept", help="Create a concept module from a given title.", metavar="title", nargs="+", type=str) parser.add_argument("-p", "--procedure", help="Create a procedure module from a given title.", metavar="title", nargs="+", type=str) parser.add_argument("-r", "--reference", help="Create a reference module from a given title.", metavar="title", nargs="+", type=str) parser.add_argument("-C", "--no-comments", help="Generate the file without any comments.", action="store_true") # Doesn't do anything right now # parser.add_argument("-d", "--module-dir", # help="Specify the directory where to save modules.", # type=str) # Get commandline arguments args = parser.parse_args() options = get_config() # Transform the args object into something that can be easily iterated args_struct = [ ("assembly", args.assembly), ("concept", args.concept), ("procedure", args.procedure), ("reference", args.reference) ] # Select all doc types for which a title has been provided valid_args = [a for a in args_struct if a[1]] # If there are no titles, print help and exit if not valid_args: parser.print_help() # If there are titles, create a new file for each one else: for doc_type, title_list in valid_args: # Doc type options accept multiple titles to create multiple files for title in title_list: create_module(title, doc_type, options, args.no_comments) if __name__ == "__main__": main()