diff options
Diffstat (limited to 'asciidoc/utils.py')
-rw-r--r-- | asciidoc/utils.py | 185 |
1 files changed, 185 insertions, 0 deletions
diff --git a/asciidoc/utils.py b/asciidoc/utils.py new file mode 100644 index 0000000..fee2c00 --- /dev/null +++ b/asciidoc/utils.py @@ -0,0 +1,185 @@ +import locale +import math +import os +import re +import time +from typing import Optional +import unicodedata + + +def userdir() -> Optional[str]: + """ + Return user's home directory or None if it is not defined. + """ + result = os.path.expanduser('~') + if result == '~': + result = None + return result + + +def file_in(fname, directory) -> bool: + """Return True if file fname resides inside directory.""" + assert os.path.isfile(fname) + # Empty directory (not to be confused with None) is the current directory. + if directory == '': + directory = os.getcwd() + else: + assert os.path.isdir(directory) + directory = os.path.realpath(directory) + fname = os.path.realpath(fname) + return os.path.commonprefix((directory, fname)) == directory + + +def assign(dst, src): + """Assign all attributes from 'src' object to 'dst' object.""" + for a, v in list(src.__dict__.items()): + setattr(dst, a, v) + + +def strip_quotes(s): + """Trim white space and, if necessary, quote characters from s.""" + s = s.strip() + # Strip quotation mark characters from quoted strings. + if len(s) >= 3 and s[0] == '"' and s[-1] == '"': + s = s[1:-1] + return s + + +def is_re(s) -> bool: + """Return True if s is a valid regular expression else return False.""" + try: + re.compile(s) + return True + except BaseException: + return False + + +def re_join(relist): + """Join list of regular expressions re1,re2,... to single regular + expression (re1)|(re2)|...""" + if len(relist) == 0: + return None + result = [] + # Delete named groups to avoid ambiguity. + for s in relist: + result.append(re.sub(r'\?P<\S+?>', '', s)) + result = ')|('.join(result) + result = '(' + result + ')' + return result + + +def lstrip_list(s): + """ + Return list with empty items from start of list removed. + """ + for i in range(len(s)): + if s[i]: + break + else: + return [] + return s[i:] + + +def rstrip_list(s): + """ + Return list with empty items from end of list removed. + """ + for i in range(len(s) - 1, -1, -1): + if s[i]: + break + else: + return [] + return s[:i + 1] + + +def strip_list(s): + """ + Return list with empty items from start and end of list removed. + """ + s = lstrip_list(s) + s = rstrip_list(s) + return s + + +def is_array(obj) -> bool: + """ + Return True if object is list or tuple type. + """ + return isinstance(obj, list) or isinstance(obj, tuple) + + +def dovetail(lines1, lines2): + """ + Append list or tuple of strings 'lines2' to list 'lines1'. Join the last + non-blank item in 'lines1' with the first non-blank item in 'lines2' into a + single string. + """ + assert is_array(lines1) + assert is_array(lines2) + lines1 = strip_list(lines1) + lines2 = strip_list(lines2) + if not lines1 or not lines2: + return list(lines1) + list(lines2) + result = list(lines1[:-1]) + result.append(lines1[-1] + lines2[0]) + result += list(lines2[1:]) + return result + + +def dovetail_tags(stag, content, etag): + """Merge the end tag with the first content line and the last + content line with the end tag. This ensures verbatim elements don't + include extraneous opening and closing line breaks.""" + return dovetail(dovetail(stag, content), etag) + + +def py2round(n, d=0): + """Utility function to get python2 rounding in python3. Python3 changed it such that + given two equally close multiples, it'll round towards the even choice. For example, + round(42.5) == 42 instead of the expected round(42.5) == 43). This function gives us + back that functionality.""" + p = 10 ** d + return float(math.floor((n * p) + math.copysign(0.5, n))) / p + + +east_asian_widths = { + 'W': 2, # Wide + 'F': 2, # Full-width (wide) + 'Na': 1, # Narrow + 'H': 1, # Half-width (narrow) + 'N': 1, # Neutral (not East Asian, treated as narrow) + 'A': 1, # Ambiguous (s/b wide in East Asian context, narrow otherwise, but that + # doesn't work) +} +"""Mapping of result codes from `unicodedata.east_asian_width()` to character +column widths.""" + + +def column_width(s): + width = 0 + for c in s: + width += east_asian_widths[unicodedata.east_asian_width(c)] + return width + + +def date_time_str(t): + """Convert seconds since the Epoch to formatted local date and time strings.""" + source_date_epoch = os.environ.get('SOURCE_DATE_EPOCH') + if source_date_epoch is not None: + t = time.gmtime(min(t, int(source_date_epoch))) + else: + t = time.localtime(t) + date_str = time.strftime('%Y-%m-%d', t) + time_str = time.strftime('%H:%M:%S', t) + if source_date_epoch is not None: + time_str += ' UTC' + elif time.daylight and t.tm_isdst == 1: + time_str += ' ' + time.tzname[1] + else: + time_str += ' ' + time.tzname[0] + # Attempt to convert the localtime to the output encoding. + try: + time_str = time_str.decode(locale.getdefaultlocale()[1]) + except Exception: + pass + return date_str, time_str |