mirror of
https://0xacab.org/anarsec/anarsec.guide.git
synced 2025-06-04 21:08:53 -04:00
python and typst script
This commit is contained in:
parent
da5f497ec1
commit
be05046783
19 changed files with 2223 additions and 0 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -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*
|
||||
|
|
111
layout/anarsec_article.typ
Normal file
111
layout/anarsec_article.typ
Normal file
|
@ -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)
|
||||
]
|
||||
]
|
||||
]
|
221
layout/python/anarsec_article_to_pdf.py
Normal file
221
layout/python/anarsec_article_to_pdf.py
Normal file
|
@ -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()
|
7
layout/python/slugify/__init__.py
Normal file
7
layout/python/slugify/__init__.py
Normal file
|
@ -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'
|
93
layout/python/slugify/__main__.py
Normal file
93
layout/python/slugify/__main__.py
Normal file
|
@ -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()
|
180
layout/python/slugify/slugify.py
Normal file
180
layout/python/slugify/slugify.py
Normal file
|
@ -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
|
47
layout/python/slugify/special.py
Normal file
47
layout/python/slugify/special.py
Normal file
|
@ -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
|
21
layout/python/text_unidecode/__init__.py
Normal file
21
layout/python/text_unidecode/__init__.py
Normal file
|
@ -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)
|
BIN
layout/python/text_unidecode/data.bin
Normal file
BIN
layout/python/text_unidecode/data.bin
Normal file
Binary file not shown.
25
layout/python/toml/__init__.py
Normal file
25
layout/python/toml/__init__.py
Normal file
|
@ -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
|
15
layout/python/toml/__init__.pyi
Normal file
15
layout/python/toml/__init__.pyi
Normal file
|
@ -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
|
1057
layout/python/toml/decoder.py
Normal file
1057
layout/python/toml/decoder.py
Normal file
File diff suppressed because it is too large
Load diff
52
layout/python/toml/decoder.pyi
Normal file
52
layout/python/toml/decoder.pyi
Normal file
|
@ -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: ...
|
304
layout/python/toml/encoder.py
Normal file
304
layout/python/toml/encoder.py
Normal file
|
@ -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)
|
34
layout/python/toml/encoder.pyi
Normal file
34
layout/python/toml/encoder.pyi
Normal file
|
@ -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): ...
|
15
layout/python/toml/ordered.py
Normal file
15
layout/python/toml/ordered.py
Normal file
|
@ -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)
|
7
layout/python/toml/ordered.pyi
Normal file
7
layout/python/toml/ordered.pyi
Normal file
|
@ -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: ...
|
24
layout/python/toml/tz.py
Normal file
24
layout/python/toml/tz.py
Normal file
|
@ -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)
|
9
layout/python/toml/tz.pyi
Normal file
9
layout/python/toml/tz.pyi
Normal file
|
@ -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): ...
|
Loading…
Add table
Add a link
Reference in a new issue