From be050467838bef7cc30f574b0bbb839769800f38 Mon Sep 17 00:00:00 2001 From: anarsec Date: Sun, 9 Jul 2023 20:53:03 +0000 Subject: [PATCH] python and typst script --- .gitignore | 1 + layout/anarsec_article.typ | 111 +++ layout/python/anarsec_article_to_pdf.py | 221 +++++ layout/python/slugify/__init__.py | 7 + layout/python/slugify/__main__.py | 93 ++ layout/python/slugify/slugify.py | 180 ++++ layout/python/slugify/special.py | 47 + layout/python/text_unidecode/__init__.py | 21 + layout/python/text_unidecode/data.bin | Bin 0 -> 311077 bytes layout/python/toml/__init__.py | 25 + layout/python/toml/__init__.pyi | 15 + layout/python/toml/decoder.py | 1057 ++++++++++++++++++++++ layout/python/toml/decoder.pyi | 52 ++ layout/python/toml/encoder.py | 304 +++++++ layout/python/toml/encoder.pyi | 34 + layout/python/toml/ordered.py | 15 + layout/python/toml/ordered.pyi | 7 + layout/python/toml/tz.py | 24 + layout/python/toml/tz.pyi | 9 + 19 files changed, 2223 insertions(+) create mode 100644 layout/anarsec_article.typ create mode 100644 layout/python/anarsec_article_to_pdf.py create mode 100644 layout/python/slugify/__init__.py create mode 100644 layout/python/slugify/__main__.py create mode 100644 layout/python/slugify/slugify.py create mode 100644 layout/python/slugify/special.py create mode 100644 layout/python/text_unidecode/__init__.py create mode 100644 layout/python/text_unidecode/data.bin create mode 100644 layout/python/toml/__init__.py create mode 100644 layout/python/toml/__init__.pyi create mode 100644 layout/python/toml/decoder.py create mode 100644 layout/python/toml/decoder.pyi create mode 100644 layout/python/toml/encoder.py create mode 100644 layout/python/toml/encoder.pyi create mode 100644 layout/python/toml/ordered.py create mode 100644 layout/python/toml/ordered.pyi create mode 100644 layout/python/toml/tz.py create mode 100644 layout/python/toml/tz.pyi diff --git a/.gitignore b/.gitignore index 19bb427..12aefe4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ public/ *.pdf +*.pyc # temporary files which can be created if a process still has a handle open of a deleted file .fuse_hidden* diff --git a/layout/anarsec_article.typ b/layout/anarsec_article.typ new file mode 100644 index 0000000..b23da0a --- /dev/null +++ b/layout/anarsec_article.typ @@ -0,0 +1,111 @@ +#let anarsec_article( + title: none, + frontimage: none, + backimage: none, + lastediteddate: none, + description: none, + content +) = { + // format links + show link: it => { + it.body + if type(it.dest) == "string" { + if it.dest.starts-with("https://") { + footnote[#it.dest.trim("https://", at: start)] + } + else if it.dest.starts-with("/glossary#") or it.dest.starts-with("/glossary/#") { + locate(location => { + let elements = query(label(it.dest.trim("/glossary#", at: start).trim("/glossary/#", at: start)), location) + text[#super[†]] + }) + } + else if it.dest.starts-with("/") { + footnote({text[anarsec.guide] + it.dest}) + } + } + else if type(it.dest) == "label" { + locate(location => { + let elements = query(it.dest, location) + text[ (#emph(elements.first().body))] + }) + } + } + + // format lists + set list(marker: ([•], [--])) + + // front cover + page()[ + #set align(center + horizon) + + #image(frontimage) + + #text(25pt, title) + ] + + // inside cover + page()[ + #set align(center + bottom) + + #text()[This version of the zine was last edited on #lastediteddate. Visit anarsec.guide to see whether it has been updated since.] + + #text()[This dagger symbol #super[†] on a word means that there is a glossary entry for it. Ai ferri corti.] + ] + + // table of contents + page()[ + #outline(indent: 20pt, depth: 3) + ] + + // content + set page(numbering: "1") + set align(left) + + pagebreak(weak: true) + + show heading.where(level: 1): it => { + pagebreak(weak: true) + block(width: 100%)[ + #set align(center) + #set text(26pt) + #smallcaps(it.body) + #v(10pt) + ] + } + show heading.where(level: 2): it => block(width: 100%)[ + #set text(19pt) + #text(it.body) + #v(10pt) + ] + show heading.where(level: 3): it => block(width: 100%)[ + #set text(14pt, weight: "bold") + #text(it.body) + #v(10pt) + ] + + content + + set page(numbering: none) + + // back cover + page()[ + #text()[ + #set align(center + horizon) + + #block(width: 100%, align(left, par(justify: true, description))) + + #image(height: 250pt, backimage) + ] + ] +} + +// blockquote function ; TODO: remove when typst has a native blockquote function (see https://github.com/typst/typst/issues/105) +#let blockquote( + content +) = align(center)[ + #block(width: 92%, fill: rgb(230, 230, 230), radius: 4pt, inset: 8pt)[ + #align(left)[ + #text(content) + ] + ] +] diff --git a/layout/python/anarsec_article_to_pdf.py b/layout/python/anarsec_article_to_pdf.py new file mode 100644 index 0000000..f806104 --- /dev/null +++ b/layout/python/anarsec_article_to_pdf.py @@ -0,0 +1,221 @@ +import argparse +import contextlib +import os +import pathlib +import re +import shutil +import slugify +import subprocess +import tempfile + +import pdfimposer +import PyPDF2 +import toml + +class Converter: + """Converts an Anarsec article to PDF booklets.""" + + def __init__(self, pandoc_binary: pathlib.Path, typst_binary: pathlib.Path, anarsec_root: pathlib.Path, post_id: str, *, force: bool = False, verbose: bool = False): + """Initialize the converter.""" + + # Set attributes + self.pandoc_binary = pandoc_binary + self.typst_binary = typst_binary + self.anarsec_root = anarsec_root + self.post_id = post_id + self.force = force + self.verbose = verbose + + # Set post directory + self.post_directory = self.anarsec_root / "content" / "posts" / self.post_id + + # Check validity of some attributes + if not self.pandoc_binary.exists() or not self.pandoc_binary.is_file(): + raise RuntimeError(f"Pandoc binary '{self.pandoc_binary}' doesn't exist or isn't a file.") + if not self.typst_binary.exists() or not self.typst_binary.is_file(): + raise RuntimeError(f"Typst binary '{self.typst_binary}' doesn't exist or isn't a file.") + if not self.anarsec_root.exists() or not self.anarsec_root.is_dir(): + raise RuntimeError(f"Anarsec root '{self.anarsec_root}' doesn't exist or isn't a directory.") + if not self.post_directory.exists() or not self.post_directory.is_dir(): + raise RuntimeError(f"Post directory '{self.post_directory}' doesn't exist or isn't a directory.") + + def convert(self): + """Convert the input file to the output file. This method should only be run once.""" + + # Set glossary file + glossary_file = self.anarsec_root / "content" / "glossary" / "_index.md" + if not glossary_file.exists() or not glossary_file.is_file(): + raise RuntimeError(f"Glossary file '{glossary_file}' doesn't exist or isn't a file.") + + # Set recommendations file + recommendations_file = self.anarsec_root / "content" / "recommendations" / "_index.md" + if not recommendations_file.exists() or not recommendations_file.is_file(): + raise RuntimeError(f"Recommendations file '{recommendations_file}' doesn't exist or isn't a file.") + + # Set input path + input_path = self.post_directory / "index.md" + if not input_path.exists() or not input_path.is_file(): + raise RuntimeError(f"Post Markdown file '{input_path}' doesn't exist or isn't a file.") + + # Load the glossary + glossary = dict() + for match in re.findall(r'### (.*?)\n+(.*?)\n*(?=###|\Z)', glossary_file.open().read(), re.DOTALL | re.MULTILINE): + glossary[slugify.slugify(match[0])] = (match[0], match[1]) + + # For each paper size + for paper_size in ["a4", "letter"]: + # Set the output path + output_path = self.post_directory / f"{self.post_id}-{paper_size}.pdf" + if not self.force and output_path.exists(): + raise RuntimeError(f"Output file '{output_path}' already exists.") + + # Work in a temporary directory + with tempfile.TemporaryDirectory() as workingDirectory: + # Copy the required resources to the working directory + shutil.copy(pathlib.Path(__file__).parent.parent / "anarsec_article.typ", workingDirectory) + for filename in input_path.parent.iterdir(): + if filename.suffix.lower() == ".webp": + subprocess.check_call(["convert", filename, pathlib.Path(workingDirectory) / f"{filename.name}.png"]) + elif filename.suffix.lower() in [".png", ".jpg", ".jpeg", ".bmp", ".svg", ".gif"]: + shutil.copy(filename, workingDirectory) + + # Separate the input file into a TOML front matter and Markdown content + with input_path.open("r") as input_file: + match = re.fullmatch(r'\+{3}\n(.*)\+{3}(.*)', input_file.read(), re.DOTALL | re.MULTILINE) + if match is None: + raise RuntimeError(f"Couldn't separate input file '{self.input_path}' into a TOML front matter and Markdown content. Is it a valid Anarsec article?") + toml_front_matter = toml.loads(match.group(1)) + markdown_content = match.group(2) + + # Grab the description + description = re.search(r'^(.*?)\<\!\-\- more \-\-\>', markdown_content, re.DOTALL | re.MULTILINE).group(1).strip("\n ") + + # Parse the description + description_md_path = pathlib.Path(workingDirectory) / "description.md" + description_txt_path = pathlib.Path(workingDirectory) / "description.txt" + description_md_path.open("w").write(description) + subprocess.check_call([str(self.pandoc_binary), "-f", "markdown", "-t", "plain", "--columns", "999999", "-o", description_txt_path, description_md_path]) + description = description_txt_path.open().read() + + # Copy the front image + front_image = pathlib.Path(workingDirectory) / ("front_image" + pathlib.Path(toml_front_matter['extra']['blogimage']).suffix) + shutil.copy(self.anarsec_root / "static" / toml_front_matter['extra']['blogimage'].removeprefix("/"), front_image) + + # Copy the back image + back_image = pathlib.Path(workingDirectory) / "back_image.png" + shutil.copy(self.anarsec_root / "static" / "images" / "gay.png", back_image) + + # Add recommendations to the Markdown content + recommendations = re.search(r'\+{3}.*?\+{3}(.*)', recommendations_file.open().read(), re.MULTILINE | re.DOTALL).group(1) + markdown_content += f"\n\n# Recommendations\n\n{recommendations}\n\n" + + # Replace all .webp images to .png images in the Markdown content + markdown_content = re.sub(r'\((.*?\.webp)\)', lambda match: f'({match.group(1)}.png)', markdown_content) + + # List glossary entries that appear in the Markdown content + glossary_entries = set() + for match in re.findall(r'\[.*?\]\(/glossary\/?#(.*?)\)', markdown_content): + glossary_entries.add(slugify.slugify(match)) + + # Add to glossary entries the glossary entries that appear in glossary entries, recursively + added_entry = True + while added_entry: + added_entry = False + for entry in list(glossary_entries): + for match in re.findall(r'\[.*?\]\((?:/glossary|)\/?#(.*?)\)', glossary[entry][1]): + new_entry = slugify.slugify(match) + if new_entry not in glossary_entries: + glossary_entries.add(new_entry) + added_entry = True + + # Add glossary entries to the Markdown content + if glossary_entries: + markdown_content += "\n\n# Glossary\n\n" + for entry, entry_content in glossary.items(): + if entry in glossary_entries: + markdown_content += f"## {entry_content[0]}\n\n{entry_content[1]}\n\n" + + # Write the Markdown content to a file + input_markdown_path = pathlib.Path(workingDirectory) / f"{self.post_id}-markdown.md" + input_markdown_path.open("w").write(markdown_content) + + # Convert the Markdown content to typst + typst_path = pathlib.Path(workingDirectory) / f"{self.post_id}.typ" + subprocess.check_call([str(self.pandoc_binary), "-f", "markdown", "-t", "typst", "--columns", "999999", "-o", typst_path, input_markdown_path]) + + # Build the full typst file + full_typst_path = pathlib.Path(workingDirectory) / f"{self.post_id}-full.typ" + full_typst = f""" +#import "anarsec_article.typ": anarsec_article, blockquote +#set page({'"a5"' if paper_size == "a4" else 'width: 5.5in, height: 8.5in'}) +#show: content => anarsec_article( + title: [ + {toml_front_matter["title"]} + ], + frontimage: "{front_image.name}", + backimage: "{back_image.name}", + lastediteddate: "{toml_front_matter["extra"]["dateedit"]}", + description: "{description}", + content +) +{typst_path.open().read()} +""" + full_typst_path.open("w").write(full_typst) + + # Convert the full typst file to PDF + pdf_path = pathlib.Path(workingDirectory) / f"{self.post_id}.pdf" + subprocess.check_call( + [str(self.typst_binary), "--root", workingDirectory, "compile", full_typst_path, pdf_path], + stderr = subprocess.STDOUT + ) + + # Insert blank pages before the back cover if needed + pdf_reader = PyPDF2.PdfFileReader(pdf_path.open("rb")) + if len(pdf_reader.pages) % 4 != 0: + pdf_writer = PyPDF2.PdfFileWriter() + for page in pdf_reader.pages[:-1]: + pdf_writer.addPage(page) + for i in range(4 - len(pdf_reader.pages) % 4): + pdf_writer.addBlankPage() + pdf_writer.addPage(pdf_reader.pages[-1]) + pdf_with_blank_pages_path = pathlib.Path(workingDirectory) / f"{self.post_id}-with-blank-pages.pdf" + pdf_writer.write(pdf_with_blank_pages_path.open("wb")) + shutil.copy(pdf_with_blank_pages_path, pdf_path) + + # Bookletize + with open(os.devnull, "w") as devnull: + with contextlib.redirect_stdout(devnull): + pdfimposer.bookletize_on_file( + pdf_path, + output_path, + layout = "2x1", + format = "A4" if paper_size == "a4" else "Letter" + ) + + # Print a message + if self.verbose: + print(f"PDF file '{output_path}' created successfully!") + +if __name__ == "__main__": + # Parse arguments + parser = argparse.ArgumentParser(description = "Converts an Anarsec article to PDF booklets.") + parser.add_argument("--pandoc-binary", type = pathlib.Path, required = True, help = "Path to the Pandoc binary. Minimum required version is 3.1.5.") + parser.add_argument("--typst-binary", type = pathlib.Path, required = True, help = "Path to the typst binary. Minimum required version is 0.6.0.") + parser.add_argument("--anarsec-root", type = pathlib.Path, required = True, help = "Root of the Anarsec repository.") + parser.add_argument("--post-id", type = str, required = True, help = "ID of the Anarsec post to convert, i.e. the name of the post folder in '/content/posts'.") + parser.add_argument("-f", "--force", dest = "force", default = False, action = "store_true", help = "Replace the output files if they already exist.") + parser.add_argument("-v", "--verbose", dest = "verbose", default = False, action = "store_true", help = "Print messages when the output files are created.") + arguments = parser.parse_args() + + # Create the converter + converter = Converter( + arguments.pandoc_binary, + arguments.typst_binary, + arguments.anarsec_root, + arguments.post_id, + force = arguments.force, + verbose = arguments.verbose + ) + + # Convert + converter.convert() diff --git a/layout/python/slugify/__init__.py b/layout/python/slugify/__init__.py new file mode 100644 index 0000000..d69e2ed --- /dev/null +++ b/layout/python/slugify/__init__.py @@ -0,0 +1,7 @@ +from .special import * +from .slugify import * + + +__author__ = 'Val Neekman @ Neekware Inc. [@vneekman]' +__description__ = 'A Python slugify application that also handles Unicode' +__version__ = '4.0.1' diff --git a/layout/python/slugify/__main__.py b/layout/python/slugify/__main__.py new file mode 100644 index 0000000..a11989b --- /dev/null +++ b/layout/python/slugify/__main__.py @@ -0,0 +1,93 @@ +from __future__ import print_function, absolute_import +import argparse +import sys + +from .slugify import slugify, DEFAULT_SEPARATOR + + +def parse_args(argv): + parser = argparse.ArgumentParser(description="Sluggify string") + + input_group = parser.add_argument_group(description="Input") + input_group.add_argument("input_string", nargs='*', + help='Text to slugify') + input_group.add_argument("--stdin", action='store_true', + help="Take the text from STDIN") + + parser.add_argument("--no-entities", action='store_false', dest='entities', default=True, + help="Do not convert HTML entities to unicode") + parser.add_argument("--no-decimal", action='store_false', dest='decimal', default=True, + help="Do not convert HTML decimal to unicode") + parser.add_argument("--no-hexadecimal", action='store_false', dest='hexadecimal', default=True, + help="Do not convert HTML hexadecimal to unicode") + parser.add_argument("--max-length", type=int, default=0, + help="Output string length, 0 for no limit") + parser.add_argument("--word-boundary", action='store_true', default=False, + help="Truncate to complete word even if length ends up shorter than --max_length") + parser.add_argument("--save-order", action='store_true', default=False, + help="When set and --max_length > 0 return whole words in the initial order") + parser.add_argument("--separator", type=str, default=DEFAULT_SEPARATOR, + help="Separator between words. By default " + DEFAULT_SEPARATOR) + parser.add_argument("--stopwords", nargs='+', + help="Words to discount") + parser.add_argument("--regex-pattern", + help="Python regex pattern for allowed characters") + parser.add_argument("--no-lowercase", action='store_false', dest='lowercase', default=True, + help="Activate case sensitivity") + parser.add_argument("--replacements", nargs='+', + help="""Additional replacement rules e.g. "|->or", "%%->percent".""") + + args = parser.parse_args(argv[1:]) + + if args.input_string and args.stdin: + parser.error("Input strings and --stdin cannot work together") + + if args.replacements: + def split_check(repl): + SEP = '->' + if SEP not in repl: + parser.error("Replacements must be of the form: ORIGINAL{SEP}REPLACED".format(SEP=SEP)) + return repl.split(SEP, 1) + args.replacements = [split_check(repl) for repl in args.replacements] + + if args.input_string: + args.input_string = " ".join(args.input_string) + elif args.stdin: + args.input_string = sys.stdin.read() + + if not args.input_string: + args.input_string = '' + + return args + + +def slugify_params(args): + return dict( + text=args.input_string, + entities=args.entities, + decimal=args.decimal, + hexadecimal=args.hexadecimal, + max_length=args.max_length, + word_boundary=args.word_boundary, + save_order=args.save_order, + separator=args.separator, + stopwords=args.stopwords, + lowercase=args.lowercase, + replacements=args.replacements + ) + + +def main(argv=None): # pragma: no cover + """ Run this program """ + if argv is None: + argv = sys.argv + args = parse_args(argv) + params = slugify_params(args) + try: + print(slugify(**params)) + except KeyboardInterrupt: + sys.exit(-1) + + +if __name__ == '__main__': # pragma: no cover + main() diff --git a/layout/python/slugify/slugify.py b/layout/python/slugify/slugify.py new file mode 100644 index 0000000..bb3aa95 --- /dev/null +++ b/layout/python/slugify/slugify.py @@ -0,0 +1,180 @@ +import re +import unicodedata +import types +import sys + +try: + from htmlentitydefs import name2codepoint + _unicode = unicode + _unicode_type = types.UnicodeType +except ImportError: + from html.entities import name2codepoint + _unicode = str + _unicode_type = str + unichr = chr + +try: + import text_unidecode as unidecode +except ImportError: + import unidecode + +__all__ = ['slugify', 'smart_truncate'] + + +CHAR_ENTITY_PATTERN = re.compile(r'&(%s);' % '|'.join(name2codepoint)) +DECIMAL_PATTERN = re.compile(r'&#(\d+);') +HEX_PATTERN = re.compile(r'&#x([\da-fA-F]+);') +QUOTE_PATTERN = re.compile(r'[\']+') +ALLOWED_CHARS_PATTERN = re.compile(r'[^-a-z0-9]+') +ALLOWED_CHARS_PATTERN_WITH_UPPERCASE = re.compile(r'[^-a-zA-Z0-9]+') +DUPLICATE_DASH_PATTERN = re.compile(r'-{2,}') +NUMBERS_PATTERN = re.compile(r'(?<=\d),(?=\d)') +DEFAULT_SEPARATOR = '-' + + +def smart_truncate(string, max_length=0, word_boundary=False, separator=' ', save_order=False): + """ + Truncate a string. + :param string (str): string for modification + :param max_length (int): output string length + :param word_boundary (bool): + :param save_order (bool): if True then word order of output string is like input string + :param separator (str): separator between words + :return: + """ + + string = string.strip(separator) + + if not max_length: + return string + + if len(string) < max_length: + return string + + if not word_boundary: + return string[:max_length].strip(separator) + + if separator not in string: + return string[:max_length] + + truncated = '' + for word in string.split(separator): + if word: + next_len = len(truncated) + len(word) + if next_len < max_length: + truncated += '{}{}'.format(word, separator) + elif next_len == max_length: + truncated += '{}'.format(word) + break + else: + if save_order: + break + if not truncated: # pragma: no cover + truncated = string[:max_length] + return truncated.strip(separator) + + +def slugify(text, entities=True, decimal=True, hexadecimal=True, max_length=0, word_boundary=False, + separator=DEFAULT_SEPARATOR, save_order=False, stopwords=(), regex_pattern=None, lowercase=True, + replacements=()): + """ + Make a slug from the given text. + :param text (str): initial text + :param entities (bool): converts html entities to unicode + :param decimal (bool): converts html decimal to unicode + :param hexadecimal (bool): converts html hexadecimal to unicode + :param max_length (int): output string length + :param word_boundary (bool): truncates to complete word even if length ends up shorter than max_length + :param save_order (bool): if parameter is True and max_length > 0 return whole words in the initial order + :param separator (str): separator between words + :param stopwords (iterable): words to discount + :param regex_pattern (str): regex pattern for allowed characters + :param lowercase (bool): activate case sensitivity by setting it to False + :param replacements (iterable): list of replacement rules e.g. [['|', 'or'], ['%', 'percent']] + :return (str): + """ + + # user-specific replacements + if replacements: + for old, new in replacements: + text = text.replace(old, new) + + # ensure text is unicode + if not isinstance(text, _unicode_type): + text = _unicode(text, 'utf-8', 'ignore') + + # replace quotes with dashes - pre-process + text = QUOTE_PATTERN.sub(DEFAULT_SEPARATOR, text) + + # decode unicode + text = unidecode.unidecode(text) + + # ensure text is still in unicode + if not isinstance(text, _unicode_type): + text = _unicode(text, 'utf-8', 'ignore') + + # character entity reference + if entities: + text = CHAR_ENTITY_PATTERN.sub(lambda m: unichr(name2codepoint[m.group(1)]), text) + + # decimal character reference + if decimal: + try: + text = DECIMAL_PATTERN.sub(lambda m: unichr(int(m.group(1))), text) + except Exception: + pass + + # hexadecimal character reference + if hexadecimal: + try: + text = HEX_PATTERN.sub(lambda m: unichr(int(m.group(1), 16)), text) + except Exception: + pass + + # translate + text = unicodedata.normalize('NFKD', text) + if sys.version_info < (3,): + text = text.encode('ascii', 'ignore') + + # make the text lowercase (optional) + if lowercase: + text = text.lower() + + # remove generated quotes -- post-process + text = QUOTE_PATTERN.sub('', text) + + # cleanup numbers + text = NUMBERS_PATTERN.sub('', text) + + # replace all other unwanted characters + if lowercase: + pattern = regex_pattern or ALLOWED_CHARS_PATTERN + else: + pattern = regex_pattern or ALLOWED_CHARS_PATTERN_WITH_UPPERCASE + text = re.sub(pattern, DEFAULT_SEPARATOR, text) + + # remove redundant + text = DUPLICATE_DASH_PATTERN.sub(DEFAULT_SEPARATOR, text).strip(DEFAULT_SEPARATOR) + + # remove stopwords + if stopwords: + if lowercase: + stopwords_lower = [s.lower() for s in stopwords] + words = [w for w in text.split(DEFAULT_SEPARATOR) if w not in stopwords_lower] + else: + words = [w for w in text.split(DEFAULT_SEPARATOR) if w not in stopwords] + text = DEFAULT_SEPARATOR.join(words) + + # finalize user-specific replacements + if replacements: + for old, new in replacements: + text = text.replace(old, new) + + # smart truncate if requested + if max_length > 0: + text = smart_truncate(text, max_length, word_boundary, DEFAULT_SEPARATOR, save_order) + + if separator != DEFAULT_SEPARATOR: + text = text.replace(DEFAULT_SEPARATOR, separator) + + return text diff --git a/layout/python/slugify/special.py b/layout/python/slugify/special.py new file mode 100644 index 0000000..d3478d5 --- /dev/null +++ b/layout/python/slugify/special.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- + + +def add_uppercase_char(char_list): + """ Given a replacement char list, this adds uppercase chars to the list """ + + for item in char_list: + char, xlate = item + upper_dict = char.upper(), xlate.capitalize() + if upper_dict not in char_list and char != upper_dict[0]: + char_list.insert(0, upper_dict) + return char_list + + +# Language specific pre translations +# Source awesome-slugify + +_CYRILLIC = [ # package defaults: + (u'ё', u'e'), # io / yo + (u'я', u'ya'), # ia + (u'х', u'h'), # kh + (u'у', u'y'), # u + (u'щ', u'sch'), # shch + (u'ю', u'u'), # iu / yu +] +CYRILLIC = add_uppercase_char(_CYRILLIC) + +_GERMAN = [ # package defaults: + (u'ä', u'ae'), # a + (u'ö', u'oe'), # o + (u'ü', u'ue'), # u +] +GERMAN = add_uppercase_char(_GERMAN) + +_GREEK = [ # package defaults: + (u'χ', u'ch'), # kh + (u'Ξ', u'X'), # Ks + (u'ϒ', u'Y'), # U + (u'υ', u'y'), # u + (u'ύ', u'y'), + (u'ϋ', u'y'), + (u'ΰ', u'y'), +] +GREEK = add_uppercase_char(_GREEK) + +# Pre translations +PRE_TRANSLATIONS = CYRILLIC + GERMAN + GREEK diff --git a/layout/python/text_unidecode/__init__.py b/layout/python/text_unidecode/__init__.py new file mode 100644 index 0000000..80282c7 --- /dev/null +++ b/layout/python/text_unidecode/__init__.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import, unicode_literals +import os +import pkgutil + +_replaces = pkgutil.get_data(__name__, 'data.bin').decode('utf8').split('\x00') + +def unidecode(txt): + chars = [] + for ch in txt: + codepoint = ord(ch) + + if not codepoint: + chars.append('\x00') + continue + + try: + chars.append(_replaces[codepoint-1]) + except IndexError: + pass + return "".join(chars) diff --git a/layout/python/text_unidecode/data.bin b/layout/python/text_unidecode/data.bin new file mode 100644 index 0000000000000000000000000000000000000000..523d4898e815eaff4d5a0b16eefb5a0a6835b0bf GIT binary patch literal 311077 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 literal 0 HcmV?d00001 diff --git a/layout/python/toml/__init__.py b/layout/python/toml/__init__.py new file mode 100644 index 0000000..7719ac2 --- /dev/null +++ b/layout/python/toml/__init__.py @@ -0,0 +1,25 @@ +"""Python module which parses and emits TOML. + +Released under the MIT license. +""" + +from toml import encoder +from toml import decoder + +__version__ = "0.10.2" +_spec_ = "0.5.0" + +load = decoder.load +loads = decoder.loads +TomlDecoder = decoder.TomlDecoder +TomlDecodeError = decoder.TomlDecodeError +TomlPreserveCommentDecoder = decoder.TomlPreserveCommentDecoder + +dump = encoder.dump +dumps = encoder.dumps +TomlEncoder = encoder.TomlEncoder +TomlArraySeparatorEncoder = encoder.TomlArraySeparatorEncoder +TomlPreserveInlineDictEncoder = encoder.TomlPreserveInlineDictEncoder +TomlNumpyEncoder = encoder.TomlNumpyEncoder +TomlPreserveCommentEncoder = encoder.TomlPreserveCommentEncoder +TomlPathlibEncoder = encoder.TomlPathlibEncoder diff --git a/layout/python/toml/__init__.pyi b/layout/python/toml/__init__.pyi new file mode 100644 index 0000000..94c20f4 --- /dev/null +++ b/layout/python/toml/__init__.pyi @@ -0,0 +1,15 @@ +from toml import decoder as decoder, encoder as encoder + +load = decoder.load +loads = decoder.loads +TomlDecoder = decoder.TomlDecoder +TomlDecodeError = decoder.TomlDecodeError +TomlPreserveCommentDecoder = decoder.TomlPreserveCommentDecoder +dump = encoder.dump +dumps = encoder.dumps +TomlEncoder = encoder.TomlEncoder +TomlArraySeparatorEncoder = encoder.TomlArraySeparatorEncoder +TomlPreserveInlineDictEncoder = encoder.TomlPreserveInlineDictEncoder +TomlNumpyEncoder = encoder.TomlNumpyEncoder +TomlPreserveCommentEncoder = encoder.TomlPreserveCommentEncoder +TomlPathlibEncoder = encoder.TomlPathlibEncoder diff --git a/layout/python/toml/decoder.py b/layout/python/toml/decoder.py new file mode 100644 index 0000000..bf400e9 --- /dev/null +++ b/layout/python/toml/decoder.py @@ -0,0 +1,1057 @@ +import datetime +import io +from os import linesep +import re +import sys + +from toml.tz import TomlTz + +if sys.version_info < (3,): + _range = xrange # noqa: F821 +else: + unicode = str + _range = range + basestring = str + unichr = chr + + +def _detect_pathlib_path(p): + if (3, 4) <= sys.version_info: + import pathlib + if isinstance(p, pathlib.PurePath): + return True + return False + + +def _ispath(p): + if isinstance(p, (bytes, basestring)): + return True + return _detect_pathlib_path(p) + + +def _getpath(p): + if (3, 6) <= sys.version_info: + import os + return os.fspath(p) + if _detect_pathlib_path(p): + return str(p) + return p + + +try: + FNFError = FileNotFoundError +except NameError: + FNFError = IOError + + +TIME_RE = re.compile(r"([0-9]{2}):([0-9]{2}):([0-9]{2})(\.([0-9]{3,6}))?") + + +class TomlDecodeError(ValueError): + """Base toml Exception / Error.""" + + def __init__(self, msg, doc, pos): + lineno = doc.count('\n', 0, pos) + 1 + colno = pos - doc.rfind('\n', 0, pos) + emsg = '{} (line {} column {} char {})'.format(msg, lineno, colno, pos) + ValueError.__init__(self, emsg) + self.msg = msg + self.doc = doc + self.pos = pos + self.lineno = lineno + self.colno = colno + + +# Matches a TOML number, which allows underscores for readability +_number_with_underscores = re.compile('([0-9])(_([0-9]))*') + + +class CommentValue(object): + def __init__(self, val, comment, beginline, _dict): + self.val = val + separator = "\n" if beginline else " " + self.comment = separator + comment + self._dict = _dict + + def __getitem__(self, key): + return self.val[key] + + def __setitem__(self, key, value): + self.val[key] = value + + def dump(self, dump_value_func): + retstr = dump_value_func(self.val) + if isinstance(self.val, self._dict): + return self.comment + "\n" + unicode(retstr) + else: + return unicode(retstr) + self.comment + + +def _strictly_valid_num(n): + n = n.strip() + if not n: + return False + if n[0] == '_': + return False + if n[-1] == '_': + return False + if "_." in n or "._" in n: + return False + if len(n) == 1: + return True + if n[0] == '0' and n[1] not in ['.', 'o', 'b', 'x']: + return False + if n[0] == '+' or n[0] == '-': + n = n[1:] + if len(n) > 1 and n[0] == '0' and n[1] != '.': + return False + if '__' in n: + return False + return True + + +def load(f, _dict=dict, decoder=None): + """Parses named file or files as toml and returns a dictionary + + Args: + f: Path to the file to open, array of files to read into single dict + or a file descriptor + _dict: (optional) Specifies the class of the returned toml dictionary + decoder: The decoder to use + + Returns: + Parsed toml file represented as a dictionary + + Raises: + TypeError -- When f is invalid type + TomlDecodeError: Error while decoding toml + IOError / FileNotFoundError -- When an array with no valid (existing) + (Python 2 / Python 3) file paths is passed + """ + + if _ispath(f): + with io.open(_getpath(f), encoding='utf-8') as ffile: + return loads(ffile.read(), _dict, decoder) + elif isinstance(f, list): + from os import path as op + from warnings import warn + if not [path for path in f if op.exists(path)]: + error_msg = "Load expects a list to contain filenames only." + error_msg += linesep + error_msg += ("The list needs to contain the path of at least one " + "existing file.") + raise FNFError(error_msg) + if decoder is None: + decoder = TomlDecoder(_dict) + d = decoder.get_empty_table() + for l in f: # noqa: E741 + if op.exists(l): + d.update(load(l, _dict, decoder)) + else: + warn("Non-existent filename in list with at least one valid " + "filename") + return d + else: + try: + return loads(f.read(), _dict, decoder) + except AttributeError: + raise TypeError("You can only load a file descriptor, filename or " + "list") + + +_groupname_re = re.compile(r'^[A-Za-z0-9_-]+$') + + +def loads(s, _dict=dict, decoder=None): + """Parses string as toml + + Args: + s: String to be parsed + _dict: (optional) Specifies the class of the returned toml dictionary + + Returns: + Parsed toml file represented as a dictionary + + Raises: + TypeError: When a non-string is passed + TomlDecodeError: Error while decoding toml + """ + + implicitgroups = [] + if decoder is None: + decoder = TomlDecoder(_dict) + retval = decoder.get_empty_table() + currentlevel = retval + if not isinstance(s, basestring): + raise TypeError("Expecting something like a string") + + if not isinstance(s, unicode): + s = s.decode('utf8') + + original = s + sl = list(s) + openarr = 0 + openstring = False + openstrchar = "" + multilinestr = False + arrayoftables = False + beginline = True + keygroup = False + dottedkey = False + keyname = 0 + key = '' + prev_key = '' + line_no = 1 + + for i, item in enumerate(sl): + if item == '\r' and sl[i + 1] == '\n': + sl[i] = ' ' + continue + if keyname: + key += item + if item == '\n': + raise TomlDecodeError("Key name found without value." + " Reached end of line.", original, i) + if openstring: + if item == openstrchar: + oddbackslash = False + k = 1 + while i >= k and sl[i - k] == '\\': + oddbackslash = not oddbackslash + k += 1 + if not oddbackslash: + keyname = 2 + openstring = False + openstrchar = "" + continue + elif keyname == 1: + if item.isspace(): + keyname = 2 + continue + elif item == '.': + dottedkey = True + continue + elif item.isalnum() or item == '_' or item == '-': + continue + elif (dottedkey and sl[i - 1] == '.' and + (item == '"' or item == "'")): + openstring = True + openstrchar = item + continue + elif keyname == 2: + if item.isspace(): + if dottedkey: + nextitem = sl[i + 1] + if not nextitem.isspace() and nextitem != '.': + keyname = 1 + continue + if item == '.': + dottedkey = True + nextitem = sl[i + 1] + if not nextitem.isspace() and nextitem != '.': + keyname = 1 + continue + if item == '=': + keyname = 0 + prev_key = key[:-1].rstrip() + key = '' + dottedkey = False + else: + raise TomlDecodeError("Found invalid character in key name: '" + + item + "'. Try quoting the key name.", + original, i) + if item == "'" and openstrchar != '"': + k = 1 + try: + while sl[i - k] == "'": + k += 1 + if k == 3: + break + except IndexError: + pass + if k == 3: + multilinestr = not multilinestr + openstring = multilinestr + else: + openstring = not openstring + if openstring: + openstrchar = "'" + else: + openstrchar = "" + if item == '"' and openstrchar != "'": + oddbackslash = False + k = 1 + tripquote = False + try: + while sl[i - k] == '"': + k += 1 + if k == 3: + tripquote = True + break + if k == 1 or (k == 3 and tripquote): + while sl[i - k] == '\\': + oddbackslash = not oddbackslash + k += 1 + except IndexError: + pass + if not oddbackslash: + if tripquote: + multilinestr = not multilinestr + openstring = multilinestr + else: + openstring = not openstring + if openstring: + openstrchar = '"' + else: + openstrchar = "" + if item == '#' and (not openstring and not keygroup and + not arrayoftables): + j = i + comment = "" + try: + while sl[j] != '\n': + comment += s[j] + sl[j] = ' ' + j += 1 + except IndexError: + break + if not openarr: + decoder.preserve_comment(line_no, prev_key, comment, beginline) + if item == '[' and (not openstring and not keygroup and + not arrayoftables): + if beginline: + if len(sl) > i + 1 and sl[i + 1] == '[': + arrayoftables = True + else: + keygroup = True + else: + openarr += 1 + if item == ']' and not openstring: + if keygroup: + keygroup = False + elif arrayoftables: + if sl[i - 1] == ']': + arrayoftables = False + else: + openarr -= 1 + if item == '\n': + if openstring or multilinestr: + if not multilinestr: + raise TomlDecodeError("Unbalanced quotes", original, i) + if ((sl[i - 1] == "'" or sl[i - 1] == '"') and ( + sl[i - 2] == sl[i - 1])): + sl[i] = sl[i - 1] + if sl[i - 3] == sl[i - 1]: + sl[i - 3] = ' ' + elif openarr: + sl[i] = ' ' + else: + beginline = True + line_no += 1 + elif beginline and sl[i] != ' ' and sl[i] != '\t': + beginline = False + if not keygroup and not arrayoftables: + if sl[i] == '=': + raise TomlDecodeError("Found empty keyname. ", original, i) + keyname = 1 + key += item + if keyname: + raise TomlDecodeError("Key name found without value." + " Reached end of file.", original, len(s)) + if openstring: # reached EOF and have an unterminated string + raise TomlDecodeError("Unterminated string found." + " Reached end of file.", original, len(s)) + s = ''.join(sl) + s = s.split('\n') + multikey = None + multilinestr = "" + multibackslash = False + pos = 0 + for idx, line in enumerate(s): + if idx > 0: + pos += len(s[idx - 1]) + 1 + + decoder.embed_comments(idx, currentlevel) + + if not multilinestr or multibackslash or '\n' not in multilinestr: + line = line.strip() + if line == "" and (not multikey or multibackslash): + continue + if multikey: + if multibackslash: + multilinestr += line + else: + multilinestr += line + multibackslash = False + closed = False + if multilinestr[0] == '[': + closed = line[-1] == ']' + elif len(line) > 2: + closed = (line[-1] == multilinestr[0] and + line[-2] == multilinestr[0] and + line[-3] == multilinestr[0]) + if closed: + try: + value, vtype = decoder.load_value(multilinestr) + except ValueError as err: + raise TomlDecodeError(str(err), original, pos) + currentlevel[multikey] = value + multikey = None + multilinestr = "" + else: + k = len(multilinestr) - 1 + while k > -1 and multilinestr[k] == '\\': + multibackslash = not multibackslash + k -= 1 + if multibackslash: + multilinestr = multilinestr[:-1] + else: + multilinestr += "\n" + continue + if line[0] == '[': + arrayoftables = False + if len(line) == 1: + raise TomlDecodeError("Opening key group bracket on line by " + "itself.", original, pos) + if line[1] == '[': + arrayoftables = True + line = line[2:] + splitstr = ']]' + else: + line = line[1:] + splitstr = ']' + i = 1 + quotesplits = decoder._get_split_on_quotes(line) + quoted = False + for quotesplit in quotesplits: + if not quoted and splitstr in quotesplit: + break + i += quotesplit.count(splitstr) + quoted = not quoted + line = line.split(splitstr, i) + if len(line) < i + 1 or line[-1].strip() != "": + raise TomlDecodeError("Key group not on a line by itself.", + original, pos) + groups = splitstr.join(line[:-1]).split('.') + i = 0 + while i < len(groups): + groups[i] = groups[i].strip() + if len(groups[i]) > 0 and (groups[i][0] == '"' or + groups[i][0] == "'"): + groupstr = groups[i] + j = i + 1 + while ((not groupstr[0] == groupstr[-1]) or + len(groupstr) == 1): + j += 1 + if j > len(groups) + 2: + raise TomlDecodeError("Invalid group name '" + + groupstr + "' Something " + + "went wrong.", original, pos) + groupstr = '.'.join(groups[i:j]).strip() + groups[i] = groupstr[1:-1] + groups[i + 1:j] = [] + else: + if not _groupname_re.match(groups[i]): + raise TomlDecodeError("Invalid group name '" + + groups[i] + "'. Try quoting it.", + original, pos) + i += 1 + currentlevel = retval + for i in _range(len(groups)): + group = groups[i] + if group == "": + raise TomlDecodeError("Can't have a keygroup with an empty " + "name", original, pos) + try: + currentlevel[group] + if i == len(groups) - 1: + if group in implicitgroups: + implicitgroups.remove(group) + if arrayoftables: + raise TomlDecodeError("An implicitly defined " + "table can't be an array", + original, pos) + elif arrayoftables: + currentlevel[group].append(decoder.get_empty_table() + ) + else: + raise TomlDecodeError("What? " + group + + " already exists?" + + str(currentlevel), + original, pos) + except TypeError: + currentlevel = currentlevel[-1] + if group not in currentlevel: + currentlevel[group] = decoder.get_empty_table() + if i == len(groups) - 1 and arrayoftables: + currentlevel[group] = [decoder.get_empty_table()] + except KeyError: + if i != len(groups) - 1: + implicitgroups.append(group) + currentlevel[group] = decoder.get_empty_table() + if i == len(groups) - 1 and arrayoftables: + currentlevel[group] = [decoder.get_empty_table()] + currentlevel = currentlevel[group] + if arrayoftables: + try: + currentlevel = currentlevel[-1] + except KeyError: + pass + elif line[0] == "{": + if line[-1] != "}": + raise TomlDecodeError("Line breaks are not allowed in inline" + "objects", original, pos) + try: + decoder.load_inline_object(line, currentlevel, multikey, + multibackslash) + except ValueError as err: + raise TomlDecodeError(str(err), original, pos) + elif "=" in line: + try: + ret = decoder.load_line(line, currentlevel, multikey, + multibackslash) + except ValueError as err: + raise TomlDecodeError(str(err), original, pos) + if ret is not None: + multikey, multilinestr, multibackslash = ret + return retval + + +def _load_date(val): + microsecond = 0 + tz = None + try: + if len(val) > 19: + if val[19] == '.': + if val[-1].upper() == 'Z': + subsecondval = val[20:-1] + tzval = "Z" + else: + subsecondvalandtz = val[20:] + if '+' in subsecondvalandtz: + splitpoint = subsecondvalandtz.index('+') + subsecondval = subsecondvalandtz[:splitpoint] + tzval = subsecondvalandtz[splitpoint:] + elif '-' in subsecondvalandtz: + splitpoint = subsecondvalandtz.index('-') + subsecondval = subsecondvalandtz[:splitpoint] + tzval = subsecondvalandtz[splitpoint:] + else: + tzval = None + subsecondval = subsecondvalandtz + if tzval is not None: + tz = TomlTz(tzval) + microsecond = int(int(subsecondval) * + (10 ** (6 - len(subsecondval)))) + else: + tz = TomlTz(val[19:]) + except ValueError: + tz = None + if "-" not in val[1:]: + return None + try: + if len(val) == 10: + d = datetime.date( + int(val[:4]), int(val[5:7]), + int(val[8:10])) + else: + d = datetime.datetime( + int(val[:4]), int(val[5:7]), + int(val[8:10]), int(val[11:13]), + int(val[14:16]), int(val[17:19]), microsecond, tz) + except ValueError: + return None + return d + + +def _load_unicode_escapes(v, hexbytes, prefix): + skip = False + i = len(v) - 1 + while i > -1 and v[i] == '\\': + skip = not skip + i -= 1 + for hx in hexbytes: + if skip: + skip = False + i = len(hx) - 1 + while i > -1 and hx[i] == '\\': + skip = not skip + i -= 1 + v += prefix + v += hx + continue + hxb = "" + i = 0 + hxblen = 4 + if prefix == "\\U": + hxblen = 8 + hxb = ''.join(hx[i:i + hxblen]).lower() + if hxb.strip('0123456789abcdef'): + raise ValueError("Invalid escape sequence: " + hxb) + if hxb[0] == "d" and hxb[1].strip('01234567'): + raise ValueError("Invalid escape sequence: " + hxb + + ". Only scalar unicode points are allowed.") + v += unichr(int(hxb, 16)) + v += unicode(hx[len(hxb):]) + return v + + +# Unescape TOML string values. + +# content after the \ +_escapes = ['0', 'b', 'f', 'n', 'r', 't', '"'] +# What it should be replaced by +_escapedchars = ['\0', '\b', '\f', '\n', '\r', '\t', '\"'] +# Used for substitution +_escape_to_escapedchars = dict(zip(_escapes, _escapedchars)) + + +def _unescape(v): + """Unescape characters in a TOML string.""" + i = 0 + backslash = False + while i < len(v): + if backslash: + backslash = False + if v[i] in _escapes: + v = v[:i - 1] + _escape_to_escapedchars[v[i]] + v[i + 1:] + elif v[i] == '\\': + v = v[:i - 1] + v[i:] + elif v[i] == 'u' or v[i] == 'U': + i += 1 + else: + raise ValueError("Reserved escape sequence used") + continue + elif v[i] == '\\': + backslash = True + i += 1 + return v + + +class InlineTableDict(object): + """Sentinel subclass of dict for inline tables.""" + + +class TomlDecoder(object): + + def __init__(self, _dict=dict): + self._dict = _dict + + def get_empty_table(self): + return self._dict() + + def get_empty_inline_table(self): + class DynamicInlineTableDict(self._dict, InlineTableDict): + """Concrete sentinel subclass for inline tables. + It is a subclass of _dict which is passed in dynamically at load + time + + It is also a subclass of InlineTableDict + """ + + return DynamicInlineTableDict() + + def load_inline_object(self, line, currentlevel, multikey=False, + multibackslash=False): + candidate_groups = line[1:-1].split(",") + groups = [] + if len(candidate_groups) == 1 and not candidate_groups[0].strip(): + candidate_groups.pop() + while len(candidate_groups) > 0: + candidate_group = candidate_groups.pop(0) + try: + _, value = candidate_group.split('=', 1) + except ValueError: + raise ValueError("Invalid inline table encountered") + value = value.strip() + if ((value[0] == value[-1] and value[0] in ('"', "'")) or ( + value[0] in '-0123456789' or + value in ('true', 'false') or + (value[0] == "[" and value[-1] == "]") or + (value[0] == '{' and value[-1] == '}'))): + groups.append(candidate_group) + elif len(candidate_groups) > 0: + candidate_groups[0] = (candidate_group + "," + + candidate_groups[0]) + else: + raise ValueError("Invalid inline table value encountered") + for group in groups: + status = self.load_line(group, currentlevel, multikey, + multibackslash) + if status is not None: + break + + def _get_split_on_quotes(self, line): + doublequotesplits = line.split('"') + quoted = False + quotesplits = [] + if len(doublequotesplits) > 1 and "'" in doublequotesplits[0]: + singlequotesplits = doublequotesplits[0].split("'") + doublequotesplits = doublequotesplits[1:] + while len(singlequotesplits) % 2 == 0 and len(doublequotesplits): + singlequotesplits[-1] += '"' + doublequotesplits[0] + doublequotesplits = doublequotesplits[1:] + if "'" in singlequotesplits[-1]: + singlequotesplits = (singlequotesplits[:-1] + + singlequotesplits[-1].split("'")) + quotesplits += singlequotesplits + for doublequotesplit in doublequotesplits: + if quoted: + quotesplits.append(doublequotesplit) + else: + quotesplits += doublequotesplit.split("'") + quoted = not quoted + return quotesplits + + def load_line(self, line, currentlevel, multikey, multibackslash): + i = 1 + quotesplits = self._get_split_on_quotes(line) + quoted = False + for quotesplit in quotesplits: + if not quoted and '=' in quotesplit: + break + i += quotesplit.count('=') + quoted = not quoted + pair = line.split('=', i) + strictly_valid = _strictly_valid_num(pair[-1]) + if _number_with_underscores.match(pair[-1]): + pair[-1] = pair[-1].replace('_', '') + while len(pair[-1]) and (pair[-1][0] != ' ' and pair[-1][0] != '\t' and + pair[-1][0] != "'" and pair[-1][0] != '"' and + pair[-1][0] != '[' and pair[-1][0] != '{' and + pair[-1].strip() != 'true' and + pair[-1].strip() != 'false'): + try: + float(pair[-1]) + break + except ValueError: + pass + if _load_date(pair[-1]) is not None: + break + if TIME_RE.match(pair[-1]): + break + i += 1 + prev_val = pair[-1] + pair = line.split('=', i) + if prev_val == pair[-1]: + raise ValueError("Invalid date or number") + if strictly_valid: + strictly_valid = _strictly_valid_num(pair[-1]) + pair = ['='.join(pair[:-1]).strip(), pair[-1].strip()] + if '.' in pair[0]: + if '"' in pair[0] or "'" in pair[0]: + quotesplits = self._get_split_on_quotes(pair[0]) + quoted = False + levels = [] + for quotesplit in quotesplits: + if quoted: + levels.append(quotesplit) + else: + levels += [level.strip() for level in + quotesplit.split('.')] + quoted = not quoted + else: + levels = pair[0].split('.') + while levels[-1] == "": + levels = levels[:-1] + for level in levels[:-1]: + if level == "": + continue + if level not in currentlevel: + currentlevel[level] = self.get_empty_table() + currentlevel = currentlevel[level] + pair[0] = levels[-1].strip() + elif (pair[0][0] == '"' or pair[0][0] == "'") and \ + (pair[0][-1] == pair[0][0]): + pair[0] = _unescape(pair[0][1:-1]) + k, koffset = self._load_line_multiline_str(pair[1]) + if k > -1: + while k > -1 and pair[1][k + koffset] == '\\': + multibackslash = not multibackslash + k -= 1 + if multibackslash: + multilinestr = pair[1][:-1] + else: + multilinestr = pair[1] + "\n" + multikey = pair[0] + else: + value, vtype = self.load_value(pair[1], strictly_valid) + try: + currentlevel[pair[0]] + raise ValueError("Duplicate keys!") + except TypeError: + raise ValueError("Duplicate keys!") + except KeyError: + if multikey: + return multikey, multilinestr, multibackslash + else: + currentlevel[pair[0]] = value + + def _load_line_multiline_str(self, p): + poffset = 0 + if len(p) < 3: + return -1, poffset + if p[0] == '[' and (p.strip()[-1] != ']' and + self._load_array_isstrarray(p)): + newp = p[1:].strip().split(',') + while len(newp) > 1 and newp[-1][0] != '"' and newp[-1][0] != "'": + newp = newp[:-2] + [newp[-2] + ',' + newp[-1]] + newp = newp[-1] + poffset = len(p) - len(newp) + p = newp + if p[0] != '"' and p[0] != "'": + return -1, poffset + if p[1] != p[0] or p[2] != p[0]: + return -1, poffset + if len(p) > 5 and p[-1] == p[0] and p[-2] == p[0] and p[-3] == p[0]: + return -1, poffset + return len(p) - 1, poffset + + def load_value(self, v, strictly_valid=True): + if not v: + raise ValueError("Empty value is invalid") + if v == 'true': + return (True, "bool") + elif v.lower() == 'true': + raise ValueError("Only all lowercase booleans allowed") + elif v == 'false': + return (False, "bool") + elif v.lower() == 'false': + raise ValueError("Only all lowercase booleans allowed") + elif v[0] == '"' or v[0] == "'": + quotechar = v[0] + testv = v[1:].split(quotechar) + triplequote = False + triplequotecount = 0 + if len(testv) > 1 and testv[0] == '' and testv[1] == '': + testv = testv[2:] + triplequote = True + closed = False + for tv in testv: + if tv == '': + if triplequote: + triplequotecount += 1 + else: + closed = True + else: + oddbackslash = False + try: + i = -1 + j = tv[i] + while j == '\\': + oddbackslash = not oddbackslash + i -= 1 + j = tv[i] + except IndexError: + pass + if not oddbackslash: + if closed: + raise ValueError("Found tokens after a closed " + + "string. Invalid TOML.") + else: + if not triplequote or triplequotecount > 1: + closed = True + else: + triplequotecount = 0 + if quotechar == '"': + escapeseqs = v.split('\\')[1:] + backslash = False + for i in escapeseqs: + if i == '': + backslash = not backslash + else: + if i[0] not in _escapes and (i[0] != 'u' and + i[0] != 'U' and + not backslash): + raise ValueError("Reserved escape sequence used") + if backslash: + backslash = False + for prefix in ["\\u", "\\U"]: + if prefix in v: + hexbytes = v.split(prefix) + v = _load_unicode_escapes(hexbytes[0], hexbytes[1:], + prefix) + v = _unescape(v) + if len(v) > 1 and v[1] == quotechar and (len(v) < 3 or + v[1] == v[2]): + v = v[2:-2] + return (v[1:-1], "str") + elif v[0] == '[': + return (self.load_array(v), "array") + elif v[0] == '{': + inline_object = self.get_empty_inline_table() + self.load_inline_object(v, inline_object) + return (inline_object, "inline_object") + elif TIME_RE.match(v): + h, m, s, _, ms = TIME_RE.match(v).groups() + time = datetime.time(int(h), int(m), int(s), int(ms) if ms else 0) + return (time, "time") + else: + parsed_date = _load_date(v) + if parsed_date is not None: + return (parsed_date, "date") + if not strictly_valid: + raise ValueError("Weirdness with leading zeroes or " + "underscores in your number.") + itype = "int" + neg = False + if v[0] == '-': + neg = True + v = v[1:] + elif v[0] == '+': + v = v[1:] + v = v.replace('_', '') + lowerv = v.lower() + if '.' in v or ('x' not in v and ('e' in v or 'E' in v)): + if '.' in v and v.split('.', 1)[1] == '': + raise ValueError("This float is missing digits after " + "the point") + if v[0] not in '0123456789': + raise ValueError("This float doesn't have a leading " + "digit") + v = float(v) + itype = "float" + elif len(lowerv) == 3 and (lowerv == 'inf' or lowerv == 'nan'): + v = float(v) + itype = "float" + if itype == "int": + v = int(v, 0) + if neg: + return (0 - v, itype) + return (v, itype) + + def bounded_string(self, s): + if len(s) == 0: + return True + if s[-1] != s[0]: + return False + i = -2 + backslash = False + while len(s) + i > 0: + if s[i] == "\\": + backslash = not backslash + i -= 1 + else: + break + return not backslash + + def _load_array_isstrarray(self, a): + a = a[1:-1].strip() + if a != '' and (a[0] == '"' or a[0] == "'"): + return True + return False + + def load_array(self, a): + atype = None + retval = [] + a = a.strip() + if '[' not in a[1:-1] or "" != a[1:-1].split('[')[0].strip(): + strarray = self._load_array_isstrarray(a) + if not a[1:-1].strip().startswith('{'): + a = a[1:-1].split(',') + else: + # a is an inline object, we must find the matching parenthesis + # to define groups + new_a = [] + start_group_index = 1 + end_group_index = 2 + open_bracket_count = 1 if a[start_group_index] == '{' else 0 + in_str = False + while end_group_index < len(a[1:]): + if a[end_group_index] == '"' or a[end_group_index] == "'": + if in_str: + backslash_index = end_group_index - 1 + while (backslash_index > -1 and + a[backslash_index] == '\\'): + in_str = not in_str + backslash_index -= 1 + in_str = not in_str + if not in_str and a[end_group_index] == '{': + open_bracket_count += 1 + if in_str or a[end_group_index] != '}': + end_group_index += 1 + continue + elif a[end_group_index] == '}' and open_bracket_count > 1: + open_bracket_count -= 1 + end_group_index += 1 + continue + + # Increase end_group_index by 1 to get the closing bracket + end_group_index += 1 + + new_a.append(a[start_group_index:end_group_index]) + + # The next start index is at least after the closing + # bracket, a closing bracket can be followed by a comma + # since we are in an array. + start_group_index = end_group_index + 1 + while (start_group_index < len(a[1:]) and + a[start_group_index] != '{'): + start_group_index += 1 + end_group_index = start_group_index + 1 + a = new_a + b = 0 + if strarray: + while b < len(a) - 1: + ab = a[b].strip() + while (not self.bounded_string(ab) or + (len(ab) > 2 and + ab[0] == ab[1] == ab[2] and + ab[-2] != ab[0] and + ab[-3] != ab[0])): + a[b] = a[b] + ',' + a[b + 1] + ab = a[b].strip() + if b < len(a) - 2: + a = a[:b + 1] + a[b + 2:] + else: + a = a[:b + 1] + b += 1 + else: + al = list(a[1:-1]) + a = [] + openarr = 0 + j = 0 + for i in _range(len(al)): + if al[i] == '[': + openarr += 1 + elif al[i] == ']': + openarr -= 1 + elif al[i] == ',' and not openarr: + a.append(''.join(al[j:i])) + j = i + 1 + a.append(''.join(al[j:])) + for i in _range(len(a)): + a[i] = a[i].strip() + if a[i] != '': + nval, ntype = self.load_value(a[i]) + if atype: + if ntype != atype: + raise ValueError("Not a homogeneous array") + else: + atype = ntype + retval.append(nval) + return retval + + def preserve_comment(self, line_no, key, comment, beginline): + pass + + def embed_comments(self, idx, currentlevel): + pass + + +class TomlPreserveCommentDecoder(TomlDecoder): + + def __init__(self, _dict=dict): + self.saved_comments = {} + super(TomlPreserveCommentDecoder, self).__init__(_dict) + + def preserve_comment(self, line_no, key, comment, beginline): + self.saved_comments[line_no] = (key, comment, beginline) + + def embed_comments(self, idx, currentlevel): + if idx not in self.saved_comments: + return + + key, comment, beginline = self.saved_comments[idx] + currentlevel[key] = CommentValue(currentlevel[key], comment, beginline, + self._dict) diff --git a/layout/python/toml/decoder.pyi b/layout/python/toml/decoder.pyi new file mode 100644 index 0000000..967d3dd --- /dev/null +++ b/layout/python/toml/decoder.pyi @@ -0,0 +1,52 @@ +from toml.tz import TomlTz as TomlTz +from typing import Any, Optional + +unicode = str +basestring = str +unichr = chr +FNFError = FileNotFoundError +FNFError = IOError +TIME_RE: Any + +class TomlDecodeError(ValueError): + msg: Any = ... + doc: Any = ... + pos: Any = ... + lineno: Any = ... + colno: Any = ... + def __init__(self, msg: Any, doc: Any, pos: Any) -> None: ... + +class CommentValue: + val: Any = ... + comment: Any = ... + def __init__(self, val: Any, comment: Any, beginline: Any, _dict: Any) -> None: ... + def __getitem__(self, key: Any): ... + def __setitem__(self, key: Any, value: Any) -> None: ... + def dump(self, dump_value_func: Any): ... + +def load(f: Union[str, list, IO[str]], + _dict: Type[MutableMapping[str, Any]] = ..., + decoder: TomlDecoder = ...) \ + -> MutableMapping[str, Any]: ... +def loads(s: str, _dict: Type[MutableMapping[str, Any]] = ..., decoder: TomlDecoder = ...) \ + -> MutableMapping[str, Any]: ... + +class InlineTableDict: ... + +class TomlDecoder: + def __init__(self, _dict: Any = ...) -> None: ... + def get_empty_table(self): ... + def get_empty_inline_table(self): ... + def load_inline_object(self, line: Any, currentlevel: Any, multikey: bool = ..., multibackslash: bool = ...) -> None: ... + def load_line(self, line: Any, currentlevel: Any, multikey: Any, multibackslash: Any): ... + def load_value(self, v: Any, strictly_valid: bool = ...): ... + def bounded_string(self, s: Any): ... + def load_array(self, a: Any): ... + def preserve_comment(self, line_no: Any, key: Any, comment: Any, beginline: Any) -> None: ... + def embed_comments(self, idx: Any, currentlevel: Any) -> None: ... + +class TomlPreserveCommentDecoder(TomlDecoder): + saved_comments: Any = ... + def __init__(self, _dict: Any = ...) -> None: ... + def preserve_comment(self, line_no: Any, key: Any, comment: Any, beginline: Any) -> None: ... + def embed_comments(self, idx: Any, currentlevel: Any) -> None: ... diff --git a/layout/python/toml/encoder.py b/layout/python/toml/encoder.py new file mode 100644 index 0000000..bf17a72 --- /dev/null +++ b/layout/python/toml/encoder.py @@ -0,0 +1,304 @@ +import datetime +import re +import sys +from decimal import Decimal + +from toml.decoder import InlineTableDict + +if sys.version_info >= (3,): + unicode = str + + +def dump(o, f, encoder=None): + """Writes out dict as toml to a file + + Args: + o: Object to dump into toml + f: File descriptor where the toml should be stored + encoder: The ``TomlEncoder`` to use for constructing the output string + + Returns: + String containing the toml corresponding to dictionary + + Raises: + TypeError: When anything other than file descriptor is passed + """ + + if not f.write: + raise TypeError("You can only dump an object to a file descriptor") + d = dumps(o, encoder=encoder) + f.write(d) + return d + + +def dumps(o, encoder=None): + """Stringifies input dict as toml + + Args: + o: Object to dump into toml + encoder: The ``TomlEncoder`` to use for constructing the output string + + Returns: + String containing the toml corresponding to dict + + Examples: + ```python + >>> import toml + >>> output = { + ... 'a': "I'm a string", + ... 'b': ["I'm", "a", "list"], + ... 'c': 2400 + ... } + >>> toml.dumps(output) + 'a = "I\'m a string"\nb = [ "I\'m", "a", "list",]\nc = 2400\n' + ``` + """ + + retval = "" + if encoder is None: + encoder = TomlEncoder(o.__class__) + addtoretval, sections = encoder.dump_sections(o, "") + retval += addtoretval + outer_objs = [id(o)] + while sections: + section_ids = [id(section) for section in sections.values()] + for outer_obj in outer_objs: + if outer_obj in section_ids: + raise ValueError("Circular reference detected") + outer_objs += section_ids + newsections = encoder.get_empty_table() + for section in sections: + addtoretval, addtosections = encoder.dump_sections( + sections[section], section) + + if addtoretval or (not addtoretval and not addtosections): + if retval and retval[-2:] != "\n\n": + retval += "\n" + retval += "[" + section + "]\n" + if addtoretval: + retval += addtoretval + for s in addtosections: + newsections[section + "." + s] = addtosections[s] + sections = newsections + return retval + + +def _dump_str(v): + if sys.version_info < (3,) and hasattr(v, 'decode') and isinstance(v, str): + v = v.decode('utf-8') + v = "%r" % v + if v[0] == 'u': + v = v[1:] + singlequote = v.startswith("'") + if singlequote or v.startswith('"'): + v = v[1:-1] + if singlequote: + v = v.replace("\\'", "'") + v = v.replace('"', '\\"') + v = v.split("\\x") + while len(v) > 1: + i = -1 + if not v[0]: + v = v[1:] + v[0] = v[0].replace("\\\\", "\\") + # No, I don't know why != works and == breaks + joinx = v[0][i] != "\\" + while v[0][:i] and v[0][i] == "\\": + joinx = not joinx + i -= 1 + if joinx: + joiner = "x" + else: + joiner = "u00" + v = [v[0] + joiner + v[1]] + v[2:] + return unicode('"' + v[0] + '"') + + +def _dump_float(v): + return "{}".format(v).replace("e+0", "e+").replace("e-0", "e-") + + +def _dump_time(v): + utcoffset = v.utcoffset() + if utcoffset is None: + return v.isoformat() + # The TOML norm specifies that it's local time thus we drop the offset + return v.isoformat()[:-6] + + +class TomlEncoder(object): + + def __init__(self, _dict=dict, preserve=False): + self._dict = _dict + self.preserve = preserve + self.dump_funcs = { + str: _dump_str, + unicode: _dump_str, + list: self.dump_list, + bool: lambda v: unicode(v).lower(), + int: lambda v: v, + float: _dump_float, + Decimal: _dump_float, + datetime.datetime: lambda v: v.isoformat().replace('+00:00', 'Z'), + datetime.time: _dump_time, + datetime.date: lambda v: v.isoformat() + } + + def get_empty_table(self): + return self._dict() + + def dump_list(self, v): + retval = "[" + for u in v: + retval += " " + unicode(self.dump_value(u)) + "," + retval += "]" + return retval + + def dump_inline_table(self, section): + """Preserve inline table in its compact syntax instead of expanding + into subsection. + + https://github.com/toml-lang/toml#user-content-inline-table + """ + retval = "" + if isinstance(section, dict): + val_list = [] + for k, v in section.items(): + val = self.dump_inline_table(v) + val_list.append(k + " = " + val) + retval += "{ " + ", ".join(val_list) + " }\n" + return retval + else: + return unicode(self.dump_value(section)) + + def dump_value(self, v): + # Lookup function corresponding to v's type + dump_fn = self.dump_funcs.get(type(v)) + if dump_fn is None and hasattr(v, '__iter__'): + dump_fn = self.dump_funcs[list] + # Evaluate function (if it exists) else return v + return dump_fn(v) if dump_fn is not None else self.dump_funcs[str](v) + + def dump_sections(self, o, sup): + retstr = "" + if sup != "" and sup[-1] != ".": + sup += '.' + retdict = self._dict() + arraystr = "" + for section in o: + section = unicode(section) + qsection = section + if not re.match(r'^[A-Za-z0-9_-]+$', section): + qsection = _dump_str(section) + if not isinstance(o[section], dict): + arrayoftables = False + if isinstance(o[section], list): + for a in o[section]: + if isinstance(a, dict): + arrayoftables = True + if arrayoftables: + for a in o[section]: + arraytabstr = "\n" + arraystr += "[[" + sup + qsection + "]]\n" + s, d = self.dump_sections(a, sup + qsection) + if s: + if s[0] == "[": + arraytabstr += s + else: + arraystr += s + while d: + newd = self._dict() + for dsec in d: + s1, d1 = self.dump_sections(d[dsec], sup + + qsection + "." + + dsec) + if s1: + arraytabstr += ("[" + sup + qsection + + "." + dsec + "]\n") + arraytabstr += s1 + for s1 in d1: + newd[dsec + "." + s1] = d1[s1] + d = newd + arraystr += arraytabstr + else: + if o[section] is not None: + retstr += (qsection + " = " + + unicode(self.dump_value(o[section])) + '\n') + elif self.preserve and isinstance(o[section], InlineTableDict): + retstr += (qsection + " = " + + self.dump_inline_table(o[section])) + else: + retdict[qsection] = o[section] + retstr += arraystr + return (retstr, retdict) + + +class TomlPreserveInlineDictEncoder(TomlEncoder): + + def __init__(self, _dict=dict): + super(TomlPreserveInlineDictEncoder, self).__init__(_dict, True) + + +class TomlArraySeparatorEncoder(TomlEncoder): + + def __init__(self, _dict=dict, preserve=False, separator=","): + super(TomlArraySeparatorEncoder, self).__init__(_dict, preserve) + if separator.strip() == "": + separator = "," + separator + elif separator.strip(' \t\n\r,'): + raise ValueError("Invalid separator for arrays") + self.separator = separator + + def dump_list(self, v): + t = [] + retval = "[" + for u in v: + t.append(self.dump_value(u)) + while t != []: + s = [] + for u in t: + if isinstance(u, list): + for r in u: + s.append(r) + else: + retval += " " + unicode(u) + self.separator + t = s + retval += "]" + return retval + + +class TomlNumpyEncoder(TomlEncoder): + + def __init__(self, _dict=dict, preserve=False): + import numpy as np + super(TomlNumpyEncoder, self).__init__(_dict, preserve) + self.dump_funcs[np.float16] = _dump_float + self.dump_funcs[np.float32] = _dump_float + self.dump_funcs[np.float64] = _dump_float + self.dump_funcs[np.int16] = self._dump_int + self.dump_funcs[np.int32] = self._dump_int + self.dump_funcs[np.int64] = self._dump_int + + def _dump_int(self, v): + return "{}".format(int(v)) + + +class TomlPreserveCommentEncoder(TomlEncoder): + + def __init__(self, _dict=dict, preserve=False): + from toml.decoder import CommentValue + super(TomlPreserveCommentEncoder, self).__init__(_dict, preserve) + self.dump_funcs[CommentValue] = lambda v: v.dump(self.dump_value) + + +class TomlPathlibEncoder(TomlEncoder): + + def _dump_pathlib_path(self, v): + return _dump_str(str(v)) + + def dump_value(self, v): + if (3, 4) <= sys.version_info: + import pathlib + if isinstance(v, pathlib.PurePath): + v = str(v) + return super(TomlPathlibEncoder, self).dump_value(v) diff --git a/layout/python/toml/encoder.pyi b/layout/python/toml/encoder.pyi new file mode 100644 index 0000000..194a358 --- /dev/null +++ b/layout/python/toml/encoder.pyi @@ -0,0 +1,34 @@ +from toml.decoder import InlineTableDict as InlineTableDict +from typing import Any, Optional + +unicode = str + +def dump(o: Mapping[str, Any], f: IO[str], encoder: TomlEncoder = ...) -> str: ... +def dumps(o: Mapping[str, Any], encoder: TomlEncoder = ...) -> str: ... + +class TomlEncoder: + preserve: Any = ... + dump_funcs: Any = ... + def __init__(self, _dict: Any = ..., preserve: bool = ...): ... + def get_empty_table(self): ... + def dump_list(self, v: Any): ... + def dump_inline_table(self, section: Any): ... + def dump_value(self, v: Any): ... + def dump_sections(self, o: Any, sup: Any): ... + +class TomlPreserveInlineDictEncoder(TomlEncoder): + def __init__(self, _dict: Any = ...) -> None: ... + +class TomlArraySeparatorEncoder(TomlEncoder): + separator: Any = ... + def __init__(self, _dict: Any = ..., preserve: bool = ..., separator: str = ...) -> None: ... + def dump_list(self, v: Any): ... + +class TomlNumpyEncoder(TomlEncoder): + def __init__(self, _dict: Any = ..., preserve: bool = ...) -> None: ... + +class TomlPreserveCommentEncoder(TomlEncoder): + def __init__(self, _dict: Any = ..., preserve: bool = ...): ... + +class TomlPathlibEncoder(TomlEncoder): + def dump_value(self, v: Any): ... diff --git a/layout/python/toml/ordered.py b/layout/python/toml/ordered.py new file mode 100644 index 0000000..9c20c41 --- /dev/null +++ b/layout/python/toml/ordered.py @@ -0,0 +1,15 @@ +from collections import OrderedDict +from toml import TomlEncoder +from toml import TomlDecoder + + +class TomlOrderedDecoder(TomlDecoder): + + def __init__(self): + super(self.__class__, self).__init__(_dict=OrderedDict) + + +class TomlOrderedEncoder(TomlEncoder): + + def __init__(self): + super(self.__class__, self).__init__(_dict=OrderedDict) diff --git a/layout/python/toml/ordered.pyi b/layout/python/toml/ordered.pyi new file mode 100644 index 0000000..0f4292d --- /dev/null +++ b/layout/python/toml/ordered.pyi @@ -0,0 +1,7 @@ +from toml import TomlDecoder as TomlDecoder, TomlEncoder as TomlEncoder + +class TomlOrderedDecoder(TomlDecoder): + def __init__(self) -> None: ... + +class TomlOrderedEncoder(TomlEncoder): + def __init__(self) -> None: ... diff --git a/layout/python/toml/tz.py b/layout/python/toml/tz.py new file mode 100644 index 0000000..bf20593 --- /dev/null +++ b/layout/python/toml/tz.py @@ -0,0 +1,24 @@ +from datetime import tzinfo, timedelta + + +class TomlTz(tzinfo): + def __init__(self, toml_offset): + if toml_offset == "Z": + self._raw_offset = "+00:00" + else: + self._raw_offset = toml_offset + self._sign = -1 if self._raw_offset[0] == '-' else 1 + self._hours = int(self._raw_offset[1:3]) + self._minutes = int(self._raw_offset[4:6]) + + def __deepcopy__(self, memo): + return self.__class__(self._raw_offset) + + def tzname(self, dt): + return "UTC" + self._raw_offset + + def utcoffset(self, dt): + return self._sign * timedelta(hours=self._hours, minutes=self._minutes) + + def dst(self, dt): + return timedelta(0) diff --git a/layout/python/toml/tz.pyi b/layout/python/toml/tz.pyi new file mode 100644 index 0000000..fe37aea --- /dev/null +++ b/layout/python/toml/tz.pyi @@ -0,0 +1,9 @@ +from datetime import tzinfo +from typing import Any + +class TomlTz(tzinfo): + def __init__(self, toml_offset: Any) -> None: ... + def __deepcopy__(self, memo: Any): ... + def tzname(self, dt: Any): ... + def utcoffset(self, dt: Any): ... + def dst(self, dt: Any): ...