PK! qmonospace/__init__.py__version__ = '0.1.0' PK! rrmonospace/core/__init__.pyfrom .parse import parse from .process import process from .render import render from .layout import layout __all__ = ["parse", "process", "render", "layout"] """Rendering pipeline for books .─────────────. ( markdown file ) `─────────────' │ parse() │ ▼ ┌────────────┐ Pandoc's AST is quite granular, │ Pandoc AST │ is a bit hard to navigate, └────────────┘ and contains too much information. │ process() │ ▼ ┌──────────┐ This AST is much flatter, │ mono AST │ and the data is laid out in a way └──────────┘ that will make it easier to render. │ render() │ ▼ ┌──────────┐ │ rendered │ These blocks of text are completely rendered │ blocks │ and only need to be set on a page. └──────────┘ │ layout() Lay blocks on pages, handle breaks, side notes, etc. │ ▼ ┏━━━━━━━━━━┓ ┃ rendered ┃ ┃ book ┃ ┗━━━━━━━━━━┛ """ PK!R]C77!monospace/core/domain/__init__.pyfrom .settings import Settings __all__ = ["Settings"] PK!$2fmonospace/core/domain/blocks.pyfrom typing import List from dataclasses import dataclass, field @dataclass class Block: main: List[str] side: List[str] = field(default_factory=list) side_offset: int = 0 block_offset: int = 1 PK!H99!monospace/core/domain/document.pyfrom typing import List, Union from dataclasses import dataclass, field @dataclass class TextElement: children: List["Element"] class StructureElement: pass TextElements = List[Union[TextElement, str]] @dataclass class Text: elements: TextElements notes: List["Text"] = field(default_factory=list) Element = Union[StructureElement, TextElement, "Space", str] # --- Structure Elements ------------------------------------------------------ @dataclass class Chapter(StructureElement): title: Text @dataclass class SubChapter(StructureElement): title: Text subtitle: Text @dataclass class Section(StructureElement): title: Text @dataclass class Paragraph(StructureElement): text: Text @dataclass class Quote(StructureElement): text: Text @dataclass class OrderedList(StructureElement): # Pandoc SHOULD only provide StructureElements here list_elements: List[List[Element]] @dataclass class UnorderedList(StructureElement): # Pandoc SHOULD only provide StructureElements here list_elements: List[List[Element]] @dataclass class Unprocessed(StructureElement): kind: str # --- Text Elements ----------------------------------------------------------- @dataclass class Italic(TextElement): pass @dataclass class Bold(TextElement): pass @dataclass class CrossRef(TextElement): identifier: str @dataclass class Code(TextElement): pass @dataclass class Quoted(TextElement): pass Note = object() @dataclass class Space: def __repr__(self): return "_" space = Space() PK!tbqq!monospace/core/domain/settings.pyfrom dataclasses import dataclass @dataclass class Settings: page_width: int page_height: int margin_top: int margin_inside: int margin_outside: int margin_bottom: int side_spacing: int tab_size: int @property def editable_width(self): return ( self.page_width - self.margin_inside - self.margin_outside ) @property def main_width(self): return int(0.75 * (self.editable_width - self.side_spacing)) @property def side_width(self): return self.editable_width - self.side_spacing - self.main_width PK!%monospace/core/formatting/__init__.pyfrom .formatter import Formatter, FormatTag from .ansi import AnsiFormatter from .postscript import PostScriptFormatter __all__ = ["Formatter", "AnsiFormatter", "PostScriptFormatter", "FormatTag"] PK!уqq!monospace/core/formatting/ansi.pyfrom .formatter import Formatter def csi(params, end): return "\033[%s%s" % (";".join(str(p) for p in params), end) codes = { "Bold": (csi([1], "m"), csi([22], "m")), "Italic": (csi([3], "m"), csi([23], "m")), } class AnsiFormatter(Formatter): @staticmethod def format_tag(tag): return codes.get(tag.kind, ("", ""))[int(not tag.open)] PK!x.[[&monospace/core/formatting/formatter.pyfrom abc import ABC, abstractmethod from dataclasses import dataclass @dataclass class FormatTag: kind: str open: bool = True @property def close_tag(self): return FormatTag(kind=self.kind, open=False) class Formatter(ABC): @staticmethod @abstractmethod def format_tag(tag: FormatTag) -> str: pass PK!V?RR'monospace/core/formatting/postscript.pyfrom .formatter import Formatter class PostScriptFormatter(Formatter): pass PK!#monospace/core/formatting/styles.pyfrom ..symbols import characters def character_map(string, alphabet): return "".join(map(lambda char: alphabet.get(char, char), string)) def number_map(number_string, alphabet): n = int(number_string) return alphabet[n] + (" " if n <= 20 else "") def number_map2(number_string, alphabet): return "".join(map(lambda digit: alphabet[int(digit)], number_string)) def small_caps(string): return character_map(string.upper(), characters.small_caps) def monospace(string): return character_map(string, characters.monospace) def circled(string): try: number = int(string) return number_map(number, characters.circled_numbers) except ValueError: return character_map(string, characters.circled_characters) def fraction(nominator, denominator=None): if denominator is None: nominator, _, denominator = nominator.partition("/") return characters.fractions.get( (nominator, denominator), ( number_map2(nominator, characters.superscript) + characters.fraction_slash + number_map2(denominator, characters.subscript) ) ) PK!monospace/core/layout.pydef layout(): pass PK!҆a..monospace/core/parse.pyimport json import pypandoc # type: ignore try: pypandoc._ensure_pandoc_path() except OSError: pypandoc.download_pandoc() pypandoc._ensure_pandoc_path() def parse(source_filename) -> dict: raw_ast: str = pypandoc.convert_file(source_filename, "json") return json.loads(raw_ast) PK!C2monospace/core/process.pyfrom typing import Optional, Any, Dict, List, Tuple from .domain import document as d from .formatting import styles from .symbols.characters import double_quotes, single_quotes def process(ast: dict) -> Tuple[Dict[str, str], List[d.Element]]: # TODO: Add support for settings for the typesetting and metadata # meta = ast["meta"] processor = Processor(ast) cross_references = processor.cross_references document_elements = processor.processed return cross_references, document_elements class Processor(object): def __init__(self, ast: dict) -> None: self.cross_references = self.find_references(ast["blocks"]) # FIXME: This is just for the mockup self.cross_references.update({ "how-to-pay": "How to pay", "table-of-contents": "Table of contents", "body-text": "Body text", "point-size": "Point size", "line-spacing": "Line spacing", "line-length": "Line length", "page-margins": "Page margins", "typewriter-habit": "Typewriter habit", "system-fonts": "System fonts", "free-fonts": "Free fonts", "font-recommendations": "Font recommendations", "times-new-roman": "Times New Roman", "arial": "Arial", "summary-of-key-rules": "Summary of key rules", "foreword": "Foreword", }) self.processed = self.process_elements(ast["blocks"]) def find_references(self, elements: list) -> Dict[str, str]: references: Dict[str, str] = {} for element in elements: if element["t"] == "Header": identifier = Metadata(element["c"][1]).identifier title = join(self.process_elements(element["c"][2])) assert identifier not in references,\ "A header with this title already exists: %s" % title references[identifier] = title return references def process_elements(self, elements) -> List[d.Element]: processed = [ self.process_element(e["t"], e["c"] if "c" in e else None) for e in elements ] return [pe for pe in processed if pe is not None] def process_element(self, kind: str, value: Any) -> Optional[d.Element]: # --- Structural ------------------------------------------------------ if kind == "Header": return self.process_header(value) elif kind == "Para" or kind == "Plain": return self.process_paragraph(value) elif kind == "BlockQuote": return self.process_quote(value) elif kind == "OrderedList": return d.OrderedList( [self.process_elements(elements) for elements in value[1]]) elif kind == "BulletList": return d.UnorderedList( [self.process_elements(elements) for elements in value]) # --- Textual --------------------------------------------------------- elif kind == "Str": return value elif kind == "Strong": return d.Bold(children=self.process_elements(value)) elif kind == "Emph": return d.Italic(children=self.process_elements(value)) elif kind == "Link": return self.process_link(value) elif kind == "Code": return d.Code([styles.monospace(value[1])]) elif kind == "Quoted": return self.process_quoted(value) elif kind == "Space": return d.space return d.Unprocessed(kind) def make_text(self, elements): return d.Text( elements=self.process_elements(elements), notes=[] # TODO: populate this ) def process_paragraph(self, value): return d.Paragraph(self.make_text(value)) def process_quote(self, value): return d.Quote(self.make_text(value)) def process_header(self, value): level = value[0] metadata = Metadata(value[1]) if "subtitle" in metadata.attributes: subtitle = d.Text(metadata.attributes["subtitle"].split()) text = self.make_text(value[2]) assert level in (1, 2, 3), "Hedings must be of level 1, 2 or 3" if level == 1: return d.Chapter(title=text) elif level == 2: return d.SubChapter(title=text, subtitle=subtitle) else: return d.Section(title=text) def process_link(self, value): if value[2] and value[2][0].startswith("#"): identifier = value[2][0][1:] assert identifier in self.cross_references,\ "Link points to unknown reference '%s'" % identifier title = styles.small_caps(self.cross_references[identifier]) return d.CrossRef( children=[title], identifier=identifier, ) else: # Link's text: self.process_elements(value[1]) return d.Unprocessed("TrueLink") def process_quoted(self, value): quotes = double_quotes if value[0]["t"] == "SingleQuote": quotes = single_quotes elements = self.process_elements(value[1]) return d.Quoted(children=[quotes[0]] + elements + [quotes[1]]) class Metadata(object): def __init__(self, metadata): self.identifier: str = metadata[0] self.classes: List[str] = metadata[1] self.attributes: Dict[str, str] = dict(metadata[2]) def join(elements: List[d.Element]) -> str: def do_join(elements): result = [] for element in elements: if isinstance(element, str): result.append(element) elif hasattr(element, "elements"): result.extend(do_join(element.elements)) elif hasattr(element, "list_elements"): for _elements in element.list_elements: result.extend(do_join(_elements)) elif hasattr(element, "children"): result.extend(element.children) return result return " ".join(do_join(elements)) PK!޻monospace/core/render.pyfrom typing import Dict, List, Optional, Type from dataclasses import replace from .domain import document as d from .domain import blocks as b from .domain import Settings from .rendering import paragraph as p from .formatting import Formatter, styles """Split a document into granular rendered blocks Render all elements of a document and produce blocks. Blocks indicate where a page can break, for example: An OrderedList with two list entries, each having two paragraphs of their own, should produce 4 rendered blocks. Every Block has two parts: a main part, and a side part. The side part will be set in the margin by the main renderer. A side part contains: - Sub-chapter headers with their subtitle - Footnotes - Figure, code blocks and table captions Each of these side-elements are associated with an offset, indicating at which line of the main part they were encountered. An algorithm will spread them out in the side block, trying as much as possible to put them near their desired offset. Blocks also contain two offset values: - The block offset tells the main renderer how many spaces to put relative to the previous set block. For example, new sub-chapters should have a block offset of 3, to leave some space after the previous section. Chapters have a block offset of -3: the main renderer will only put a chapter at the beginning of a page, and the title of the chapter will start above the line where normal paragraphs start. - The side offset tells the main renderer where to place the side block _relative_ to the main block. This is mainly for sub-chapters, which have a horizontal line above the title that should not be placed at the same height as the text of the main block. Side blocks are rendered without padding and alignment: it is delegated to the main renderer, because the side blocks need to be aligned left or right depending on which page they are on. """ def render( elements: List[d.Element], settings: Settings, cross_references: Dict[str, str], formatter: Optional[Type[Formatter]] = None ) -> List[b.Block]: renderer = Renderer(settings, cross_references, formatter) return renderer.render_elements(elements) class Renderer(object): def __init__(self, settings, cross_references, formatter=None): self.settings: Settings = settings self.cross_references: Dict[str, str] = cross_references self.formatter = formatter def render_elements(self, elements) -> List[b.Block]: blocks: List[b.Block] = [] for element in elements: if isinstance(element, d.Chapter): blocks.append(self.render_chapter(element)) if isinstance(element, d.SubChapter): pass if isinstance(element, d.Section): blocks.append(self.render_section(element)) if isinstance(element, d.Paragraph): blocks.append(self.render_paragraph(element)) if isinstance(element, d.Quote): pass if isinstance(element, d.OrderedList): blocks.extend(self.render_list(element, ordered=True)) if isinstance(element, d.UnorderedList): blocks.extend(self.render_list(element, ordered=False)) if isinstance(element, d.Unprocessed): pass return blocks def render_list(self, ordered_list, ordered=False): blocks = [] # Create sub-renderer that will create thinner blocks renderer = Renderer( settings=replace( self.settings, page_width=self.settings.page_width - self.settings.tab_size ), cross_references=self.cross_references, formatter=self.formatter ) # Render all list entries and indent them # If sub-renderers also render nested lists, they will indent them # so the indentation adds up, no need to count levels :) # TODO: Add bullets/numbers for i, elements in enumerate(ordered_list.list_elements): sub_blocks = renderer.render_elements(elements) indent = " " * self.settings.tab_size for block in sub_blocks: for j, line in enumerate(block.main): block.main[j] = indent + line # Decorate bullet = "•" if ordered: bullet = styles.circled(i) first_line = sub_blocks[0].main[0] decorated_line = bullet + first_line[len(bullet):] sub_blocks[0].main[0] = decorated_line blocks.extend(sub_blocks) return blocks def render_chapter(self, chapter): elements = [d.Bold([d.Italic(chapter.title.elements)])] lines = p.align( text_elements=elements, alignment=p.Alignment.left, width=self.settings.main_width, formatter=self.formatter ) lines.insert(0, "━" * self.settings.main_width) # TODO: Notes return b.Block(main=lines, block_offset=-2) def render_section(self, section): elements = [d.Bold(section.title.elements)] lines = p.align( text_elements=elements, alignment=p.Alignment.left, width=self.settings.main_width, formatter=self.formatter, text_filter=styles.small_caps ) # TODO: Notes return b.Block(main=lines) def render_paragraph(self, paragraph): lines = p.align( text_elements=paragraph.text.elements, alignment=p.Alignment.left, width=self.settings.main_width, formatter=self.formatter ) # TODO: Notes return b.Block(main=lines) PK!$monospace/core/rendering/__init__.pyPK!=99%monospace/core/rendering/paragraph.pyimport pyphen # type: ignore import random from enum import Enum from typing import List, Union, Optional, Type, Callable from ..domain import document as d from ..formatting import Formatter, FormatTag random.seed(1337) Alignment = Enum("Alignment", ["left", "center", "right", "justify"]) # TODO: Language in settings for hyphen dictionary pyphen_dictionary = pyphen.Pyphen(lang="en_US") wrap = pyphen_dictionary.wrap Line = List[Union[FormatTag, str]] def flatten(elements: d.TextElements) -> Line: result: List[Union[FormatTag, str]] = [] for element in elements: if isinstance(element, str): result.append(element) elif isinstance(element, d.Space): result.append(element) elif isinstance(element, d.Unprocessed): result.append("" % element.kind) else: tag = FormatTag(element.__class__.__name__) result.append(tag) result.extend(flatten(element.children)) # type: ignore result.append(tag.close_tag) return result # FIXME: Bug: punctuation can be pushed to the next line on its own def align( text_elements: List[Union[d.TextElement, str]], alignment: Alignment, width: int, formatter: Optional[Type[Formatter]] = None, text_filter: Callable[[str], str] = lambda s: s ) -> List[str]: elements = flatten(text_elements) lines: List[Line] = [[]] open_tags: List[str] = [] non_word_buffer: List[Union[d.Space, FormatTag]] = [] def line_length(line: Line, with_spaces=False) -> int: only_words = [e for e in line if isinstance(e, str)] length_words = sum(len(word) for word in only_words) if with_spaces: return len(only_words) + length_words return length_words def room_left(line: Line) -> int: return width - line_length(line, with_spaces=True) def process_buffer(line): for element in non_word_buffer: if isinstance(element, FormatTag): tag = element if tag.open: open_tags.append(tag.kind) else: last_index = next( i for i, k in list(enumerate(open_tags))[::-1] if k == tag.kind ) open_tags.pop(last_index) line.append(tag) elif isinstance(element, d.Space): line.append(element) non_word_buffer.clear() def end_line(next_word=None, also_process_buffer=False): next_line = [] # Close all unclosed tags, and re-open them on the next line for kind in reversed(open_tags): lines[-1].append(FormatTag(kind=kind).close_tag) for kind in open_tags: next_line.append(FormatTag(kind=kind)) if next_word: if also_process_buffer: process_buffer(next_line) next_line.append(next_word) lines.append(next_line) # --- Step 1 -------------------------------------------------------------- # Break up elements in lines (with hyphenation) # and cross tags over the line when tags are still open. for element in elements: line = lines[-1] if not isinstance(element, str): non_word_buffer.append(element) continue word: str = element available = room_left(line) if len(word) <= available: process_buffer(line) line.append(word) else: hyphenized = wrap(word, available) if hyphenized: left, right = hyphenized process_buffer(line) line.append(left) end_line(next_word=right) else: # We are breaking the line, we don't want to put # the trailing spaces at the beginning of the next line non_word_buffer = [ e for e in non_word_buffer if not isinstance(e, d.Space) ] end_line(next_word=word, also_process_buffer=True) # No more word was on the line, but maybe some tags were there process_buffer(lines[-1]) # --- Step 2 -------------------------------------------------------------- # Depending on alignment, insert appropriate amount of spaces between words for line in lines: if alignment == Alignment.justify: pass else: # Alignment is either left or right: # there will be only one space between words. # Replace all Space object with single spaces: for i, e in enumerate(line): if isinstance(e, d.Space): line[i] = " " # --- Step 3 -------------------------------------------------------------- # Add necessary padding for line in lines: padding = " " * (width - line_length(line)) if alignment == Alignment.left: line.append(padding) elif alignment == Alignment.right: line.insert(0, padding) elif alignment == Alignment.center: middle = len(padding) // 2 left_padding = padding[:middle] right_padding = padding[middle:] line.insert(0, left_padding) line.append(right_padding) # --- Step 4 -------------------------------------------------------------- # Finalize each line with formatting def format(elem): if isinstance(elem, str): return text_filter(elem) else: return formatter.format_tag(elem) def strings(line): if formatter is not None: return [format(elem) for elem in line] else: return [elem for elem in line if isinstance(elem, str)] joined = ["".join(strings(line)) for line in lines] return joined PK!oy @@"monospace/core/symbols/__init__.pyfrom .lines import lines, Styles __all__ = ["lines", "Styles"] PK!Ǧ3 $monospace/core/symbols/characters.py# flake8: noqa circled_numbers = ["⓪"] + [ "①", "②", "③", "④", "⑤", "⑥", "⑦", "⑧", "⑨", "⑩", "⑪", "⑫", "⑬", "⑭", "⑮", "⑯", "⑰", "⑱", "⑲", "⑳", "㉑", "㉒", "㉓", "㉔", "㉕", "㉖", "㉗", "㉘", "㉙", "㉚", "㉛", "㉜", "㉝", "㉞", "㉟", "㊱", "㊲", "㊳", "㊴", "㊵", "㊶", "㊷", "㊸", "㊹", "㊺", "㊻", "㊼", "㊽", "㊾", "㊿", ] negative_circled_numbers = ["⓿"] + [ "❶", "❷", "❸", "❹", "❺", "❻", "❼", "❽", "❾", "❿", "⓫", "⓬", "⓭", "⓮", "⓯", "⓰", "⓱", "⓲", "⓳", "⓴", ] fractions = { ("1", "2"): "½", ("0", "3"): "↉", ("1", "3"): "⅓", ("2", "3"): "⅔", ("1", "4"): "¼", ("3", "4"): "¾", ("1", "5"): "⅕", ("2", "5"): "⅖", ("3", "5"): "⅗", ("4", "5"): "⅘", ("1", "6"): "⅙", ("5", "6"): "⅚", ("1", "7"): "⅐", ("1", "8"): "⅛", ("3", "8"): "⅜", ("5", "8"): "⅝", ("7", "8"): "⅞", ("1", "9"): "⅑", ("1", "10"): "⅒", } superscript = ["⁰", "¹", "²", "³", "⁴", "⁵", "⁶", "⁷", "⁸", "⁹"] subscript = ["₀", "₁", "₂", "₃", "₄", "₅", "₆", "₇", "₈", "₉"] fraction_slash = "⁄" circled_characters = { "A": "Ⓐ", "B": "Ⓑ", "C": "Ⓒ", "D": "Ⓓ", "E": "Ⓔ", "F": "Ⓕ", "G": "Ⓖ", "H": "Ⓗ", "I": "Ⓘ", "J": "Ⓙ", "K": "Ⓚ", "L": "Ⓛ", "M": "Ⓜ", "N": "Ⓝ", "O": "Ⓞ", "P": "Ⓟ", "Q": "Ⓠ", "R": "Ⓡ", "S": "Ⓢ", "T": "Ⓣ", "U": "Ⓤ", "V": "Ⓥ", "W": "Ⓦ", "X": "Ⓧ", "Y": "Ⓨ", "Z": "Ⓩ", "a": "ⓐ", "b": "ⓑ", "c": "ⓒ", "d": "ⓓ", "e": "ⓔ", "f": "ⓕ", "g": "ⓖ", "h": "ⓗ", "i": "ⓘ", "j": "ⓙ", "k": "ⓚ", "l": "ⓛ", "m": "ⓜ", "n": "ⓝ", "o": "ⓞ", "p": "ⓟ", "q": "ⓠ", "r": "ⓡ", "s": "ⓢ", "t": "ⓣ", "u": "ⓤ", "v": "ⓥ", "w": "ⓦ", "x": "ⓧ", "y": "ⓨ", "z": "ⓩ", } circled_characters.update( {str(i): char for i, char in enumerate(circled_numbers[:10])} ) monospace = { "A": "𝙰", "B": "𝙱", "C": "𝙲", "D": "𝙳", "E": "𝙴", "F": "𝙵", "G": "𝙶", "H": "𝙷", "I": "𝙸", "J": "𝙹", "K": "𝙺", "L": "𝙻", "M": "𝙼", "N": "𝙽", "O": "𝙾", "P": "𝙿", "Q": "𝚀", "R": "𝚁", "S": "𝚂", "T": "𝚃", "U": "𝚄", "V": "𝚅", "W": "𝚆", "X": "𝚇", "Y": "𝚈", "Z": "𝚉", "a": "𝚊", "b": "𝚋", "c": "𝚌", "d": "𝚍", "e": "𝚎", "f": "𝚏", "g": "𝚐", "h": "𝚑", "i": "𝚒", "j": "𝚓", "k": "𝚔", "l": "𝚕", "m": "𝚖", "n": "𝚗", "o": "𝚘", "p": "𝚙", "q": "𝚚", "r": "𝚛", "s": "𝚜", "t": "𝚝", "u": "𝚞", "v": "𝚟", "w": "𝚠", "x": "𝚡", "y": "𝚢", "z": "𝚣", "0": "𝟶", "1": "𝟷", "2": "𝟸", "3": "𝟹", "4": "𝟺", "5": "𝟻", "6": "𝟼", "7": "𝟽", "8": "𝟾", "9": "𝟿", } # Q and X missing from unicode # (but small x looks like a small cap X anyway) small_caps = { "A": "ᴀ", "B": "ʙ", "C": "ᴄ", "D": "ᴅ", "E": "ᴇ", "F": "ꜰ", "G": "ɢ", "H": "ʜ", "I": "ɪ", "J": "ᴊ", "K": "ᴋ", "L": "ʟ", "M": "ᴍ", "N": "ɴ", "O": "ᴏ", "P": "ᴘ", "Q": "Q", "R": "ʀ", "S": "ꜱ", "T": "ᴛ", "U": "ᴜ", "V": "ᴠ", "W": "ᴡ", "X": "x", "Y": "ʏ", "Z": "ᴢ", } small_caps.update( {str(i): char for i, char in enumerate(subscript)} ) double_quotes = "“”" single_quotes = "‘’" heavy_double_quotes = "❝❞" heavy_single_quotes = "❛❜" PK!E_llmonospace/core/symbols/lines.py# flake8: noqa from enum import Enum Styles = Enum("Styles", ["empty", "light", "heavy", "double", "soft", "dash2", "dash3", "dash4"]) empty = Styles.empty light = Styles.light heavy = Styles.heavy double = Styles.double soft = Styles.soft dash2 = Styles.dash2 dash3 = Styles.dash3 dash4 = Styles.dash4 # Format: (left, top, right, bottom) lines = { (light, empty, light, empty ): "─", (empty, light, empty, light ): "│", (heavy, empty, heavy, empty ): "━", (empty, heavy, empty, heavy ): "┃", (double, empty, double, empty ): "═", (empty, double, empty, double): "║", (dash2, empty, dash2, empty ): "╌", (empty, dash2, empty, dash2 ): "╎", (dash2, empty, dash2, empty ): "╍", (empty, dash2, empty, dash2 ): "╏", (dash3, empty, dash3, empty ): "┄", (empty, dash3, empty, dash3 ): "┆", (dash3, empty, dash3, empty ): "┅", (empty, dash3, empty, dash3 ): "┇", (dash4, empty, dash4, empty ): "┈", (empty, dash4, empty, dash4 ): "┊", (dash4, empty, dash4, empty ): "┉", (empty, dash4, empty, dash4 ): "┋", (light, empty, heavy, empty ): "╼", (empty, light, empty, heavy ): "╽", (heavy, empty, light, empty ): "╾", (empty, heavy, empty, light ): "╿", (light, empty, empty, empty ): "╴", (heavy, empty, empty, empty ): "╸", (empty, light, empty, empty ): "╵", (empty, heavy, empty, empty ): "╹", (empty, empty, light, empty ): "╶", (empty, empty, heavy, empty ): "╺", (empty, empty, empty, light ): "╷", (empty, empty, empty, heavy ): "╻", (empty, empty, light, light ): "┌", (light, empty, empty, light ): "┐", (empty, empty, heavy, heavy ): "┏", (heavy, empty, empty, heavy ): "┓", (empty, empty, double, double): "╔", (double, empty, empty, double): "╗", (empty, empty, soft, soft ): "╭", (soft, empty, empty, soft ): "╮", (empty, empty, heavy, light ): "┍", (heavy, empty, empty, light ): "┑", (empty, empty, light, heavy ): "┎", (light, empty, empty, heavy ): "┒", (empty, empty, double, light ): "╒", (double, empty, empty, light ): "╕", (empty, empty, light, double): "╓", (light, empty, empty, double): "╖", (empty, light, light, empty ): "└", (light, light, empty, empty ): "┘", (empty, heavy, heavy, empty ): "┗", (heavy, heavy, empty, empty ): "┛", (empty, double, double, empty ): "╚", (double, double, empty, empty ): "╝", (empty, soft, soft, empty ): "╰", (soft, soft, empty, empty ): "╯", (empty, light, heavy, empty ): "┕", (heavy, light, empty, empty ): "┙", (empty, heavy, light, empty ): "┖", (light, heavy, empty, empty ): "┚", (empty, light, double, empty ): "╘", (double, light, empty, empty ): "╛", (empty, double, light, empty ): "╙", (light, double, empty, empty ): "╜", (empty, light, light, light ): "├", (light, light, empty, light ): "┤", (empty, heavy, heavy, heavy ): "┣", (heavy, heavy, empty, heavy ): "┫", (empty, double, double, double): "╠", (double, double, empty, double): "╣", (empty, light, double, light ): "╞", (double, light, empty, light ): "╡", (empty, double, light, double): "╟", (light, double, empty, double): "╢", (empty, light, heavy, light ): "┝", (heavy, light, empty, light ): "┥", (empty, heavy, light, light ): "┞", (light, heavy, empty, light ): "┦", (empty, light, light, heavy ): "┟", (light, light, empty, heavy ): "┧", (empty, heavy, light, heavy ): "┠", (light, heavy, empty, heavy ): "┨", (empty, heavy, heavy, light ): "┡", (heavy, heavy, empty, light ): "┩", (empty, light, heavy, heavy ): "┢", (heavy, light, empty, heavy ): "┪", (light, empty, light, light ): "┬", (light, light, light, empty ): "┴", (heavy, empty, heavy, heavy ): "┳", (heavy, heavy, heavy, empty ): "┻", (double, empty, double, double): "╦", (double, double, double, empty ): "╩", (double, empty, double, light ): "╤", (double, light, double, empty ): "╧", (light, empty, light, double): "╥", (light, double, light, empty ): "╨", (heavy, empty, light, light ): "┭", (heavy, light, light, empty ): "┵", (light, empty, heavy, light ): "┮", (light, light, heavy, empty ): "┶", (heavy, empty, heavy, light ): "┯", (heavy, light, heavy, empty ): "┷", (light, empty, light, heavy ): "┰", (light, heavy, light, empty ): "┸", (heavy, empty, light, heavy ): "┱", (heavy, heavy, light, empty ): "┹", (light, empty, heavy, heavy ): "┲", (light, heavy, heavy, empty ): "┺", (light, light, light, light ): "┼", (double, double, double, double): "╬", (double, light, double, light ): "╪", (light, double, light, double): "╫", (heavy, light, light, light ): "┽", (light, heavy, heavy, light ): "╄", (light, light, heavy, light ): "┾", (heavy, light, light, heavy ): "╅", (heavy, light, heavy, light ): "┿", (light, light, heavy, heavy ): "╆", (light, heavy, light, light ): "╀", (heavy, heavy, heavy, light ): "╇", (light, light, light, heavy ): "╁", (heavy, light, heavy, heavy ): "╈", (light, heavy, light, heavy ): "╂", (heavy, heavy, light, heavy ): "╉", (heavy, heavy, light, light ): "╃", (light, heavy, heavy, heavy ): "╊", (heavy, heavy, heavy, heavy ): "╋", (empty, empty, empty, empty ): " ", } PK!HlŃTTmonospace-0.1.0.dist-info/WHEEL A н#J@Z|Jmqvh&#hڭw!Ѭ"J˫( } %PK!H(*^"monospace-0.1.0.dist-info/METADATAT͎6)>g{-[n)$qm45X[JRqӶHKr+s\ּ$].֩ pfo-Ke7P24,r%)nwo<+e_(5,Рa4 \H6oW9:KǍ4[E~˅҉vL[;ǥ͔>˿g3*̙9<[dyeJ])4ɒ,)ѰㆭKߝ3cT y}UYNRxdZR>VICpD >;aJ!t>(lupg8npɲ`2QVܾ(CY=jT/;Aõ(,ѹ$1f֣jF\h5[?_6g߿n!vPaG۽jL| n!MQQiB UlަOW GgcnG`'׃zPK!H!) monospace-0.1.0.dist-info/RECORD}ɒXнE/@B 1$6bbF_߲v`vN\24o0&|]> 0p*ʓsf<ѵQH):4ߕS؟ _^! VB}W~zM\\Sibh qS:bH`'ao g#xdp:FS/{]E:cЗ d#u X>ъغ0λ4% x4M c $"0Mle+va"8$q`Vd R@ҍfuNKL"]@?2cC|6E PA/xNe/ ̮Ck+3\ARTp͓Y}eT536z`:CBgv!;eo]emfD [}v vX":6](1/Ga2Nv<+ 81ZȤ⵺l/ɫ`6*/>9ƢIc.+c&(p30/.ʃF\]t}^FoWb\I2f>{JOZXb˟~sxգS4@p= N6h(B4