PK! qmonospace/__init__.py__version__ = '0.1.0' PK!monospace/cli/__init__.pyPK! 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!f'!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 Aside(StructureElement): elements: 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!&}!monospace/core/domain/settings.pyfrom dataclasses import dataclass @dataclass class Settings: main_width: int page_height: int side_width: int side_spacing: int tab_size: int margin_top: int margin_inside: int margin_outside: int margin_bottom: int @property def page_width(self): return ( self.margin_inside + self.margin_outside + self.side_width + self.side_spacing + self.main_width ) PK!\$$%monospace/core/formatting/__init__.pyfrom .formatter import Formatter, FormatTag, Format from .ansi import AnsiFormatter from .html import HtmlFormatter from .postscript import PostScriptFormatter __all__ = [ "AnsiFormatter", "Format", "FormatTag", "Formatter", "HtmlFormatter", "PostScriptFormatter", ] PK![F!monospace/core/formatting/ansi.pyfrom typing import List, Union from .formatter import Formatter, FormatTag, Format as F def csi(params, end): return "\033[%s%s" % (";".join(str(p) for p in params), end) def rgb(hexa): if hexa[0] == "#": hexa = hexa[1:] return int(hexa[:2], 16), int(hexa[2:4], 16), int(hexa[4:6], 16) def tag_color(tag): fg = csi([39], "m") bg = csi([49], "m") if "foreground" in tag.data: fg = csi([38, 2, *rgb(tag.data["foreground"])], "m") if "background" in tag.data: bg = csi([48, 2, *rgb(tag.data["background"])], "m") return fg + bg codes = { F.Bold: (csi([1], "m"), csi([22], "m")), F.Italic: (csi([3], "m"), csi([23], "m")), F.Color: lambda tag: tag_color(tag) if tag.open else csi([39, 49], "m") } def get_code(tag): code = codes.get(tag.kind, ("", "")) if callable(code): return code(tag) return code[not tag.open] class AnsiFormatter(Formatter): @staticmethod def format_tags(line: List[Union[FormatTag, str]]) -> str: result = "" for elem in line: if isinstance(elem, str): result += elem else: result += get_code(elem) return result PK!{ύM M &monospace/core/formatting/formatter.pyfrom enum import Enum from typing import List, Union, Any, Dict from dataclasses import dataclass, field from abc import ABCMeta, abstractmethod, abstractproperty from ..domain import Settings Format = Enum("Format", [ "Bold", "Italic", "Code", "Quoted", "CrossRef", "Color" # Warning: nesting colors will not be supported ]) @dataclass class FormatTag: kind: Format open: bool = True data: Dict[str, Any] = field(default_factory=dict) @property def close_tag(self): return FormatTag(kind=self.kind, open=False) class Formatter(metaclass=ABCMeta): """A suite of static methods for formatting a file in a given format.""" @classmethod def write_file(cls, path: str, pages: List[List[str]], settings: Settings): with open("%s.%s" % (path, cls.file_extension), "w") as f: def w(s): f.write(s) f.write("\n") w(cls.begin_file(settings)) for page in pages: w(cls.begin_page(settings)) for line in page: w(cls.format_line(line)) w(cls.end_page(settings)) w(cls.end_file(settings)) @staticmethod @abstractmethod def format_tags(line: List[Union[FormatTag, str]]) -> str: """Returns the formatting necessary for given tags. This is used in `paragraph.align`, but must also be used for any inserted text in `Renderer` or in `layout`. """ @abstractproperty def file_extension(self) -> str: pass @staticmethod @abstractmethod def begin_file(settings: Settings) -> str: """Returns the beginning of a file necessary for this format.""" @staticmethod @abstractmethod def begin_page(settings: Settings) -> str: """Returns the formatting necessary for beginning a page.""" @staticmethod @abstractmethod def format_line(line: str) -> str: """Formats a line in this format. The line must have been formatted using `format_tag` calls. """ @staticmethod @abstractmethod def end_page(settings: Settings) -> str: """Returns the formatting necessary for ending a page.""" @staticmethod @abstractmethod def end_file(settings: Settings) -> str: """Returns the end of a file necessary for this format.""" PK!:::!monospace/core/formatting/html.pyfrom typing import List, Union from .formatter import Formatter, FormatTag, Format as F tags = { F.Bold: "b", F.Italic: "i", } def tag(format_tag): return "<%s%s>" % ( "/" if not format_tag.open else "", tags[format_tag.kind] ) class HtmlFormatter(Formatter): @staticmethod def format_tags(line: List[Union[FormatTag, str]]) -> str: result = "" for elem in line: if isinstance(elem, str): result += elem else: result += tag(elem) return result PK!nx 'monospace/core/formatting/postscript.pyimport pkg_resources from jinja2 import Template from typing import List, Union, Set from .formatter import Formatter, FormatTag, Format as F from ..domain import Settings raw_template = pkg_resources.resource_string(__name__, "template.ps") prolog_template = Template(raw_template.decode("UTF-8")) font_styles = { (): "fR", (F.Bold,): "fB", (F.Italic,): "fI", (F.Bold, F.Italic): "fO" } DARK_MODE = True fg = "%d fg" % (15 if DARK_MODE else 0) # TODO: bg? class PostScriptFormatter(Formatter): file_extension = "ps" @staticmethod def format_tags(line: List[Union[FormatTag, str]]) -> str: # Regular font, open group result = "fR (" # For color styles, closing the current group and invoking the next # color is enough. # For bold / italic / bold-italic, they cannot be combined. Instead, # they are separate fonts that have to be selected. # TODO: Works for now, but closing two tags in a row will make # an empty group: ') u fO (...) u fB () u fR (' current_font_styles: Set[F] = set([]) for elem in line: if isinstance(elem, str): result += sanitize(elem) else: tag = elem if tag.kind == F.Color: if tag.open: if "foreground" in tag.data: color = tag.data["foreground"] if color[0] != "#": color = "#" + color result += ") u 16%s sethexcolor (" % color else: result += ") u %s (" % fg elif tag.kind in (F.Bold, F.Italic): if tag.open: current_font_styles.add(tag.kind) else: current_font_styles.remove(tag.kind) # Close group, render, switch to correct font, open next key = tuple( sorted(current_font_styles, key=lambda f: f.name) ) result += ") u %s (" % font_styles[key] # function u renders each character as unicode, in a regular grid return result + ") u " @staticmethod def begin_file(settings: Settings) -> str: return prolog_template.render( page_width=settings.page_width, page_height=settings.page_height ) @staticmethod def begin_page(settings: Settings) -> str: result = "" if DARK_MODE: result += "bk " return result + "tr" @staticmethod def format_line(line: str) -> str: return "%s %s n" % (fg, line) @staticmethod def end_page(settings: Settings) -> str: return "showpage" @staticmethod def end_file(settings: Settings) -> str: return r"%%EOF" def sanitize(s: str) -> str: s = s.replace("(", r"\(") s = s.replace(")", r"\)") return s PK!Qjj#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] 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!:=6=6%monospace/core/formatting/template.ps% Most of the code for this file has been copied % or adapted from a file generated with the u2ps tool, % available at https://github.com/arsv/u2ps %%BeginProlog %%BeginResource: procset unidata 2 dict dup begin /ReverseGlyphList << >> def end /unidata exch /ProcSet defineresource pop %%EndResource %%BeingResource: procset gscompat 2 dict begin % Prevent error on interpreters lacking .glyphwidth % Of course, this will produce awfully incorrect results, % but *good* replacement would be too complicated. /.glyphwidth dup where { pop pop } { { pop (M) stringwidth } bind def } ifelse % Ghostscript-specific but pretty handy routine % dict key -> value true % dict key -> false /.knownget dup where { pop pop } { { 1 index 1 index known not { pop pop false } { get true } ifelse } def } ifelse currentdict end /gscompat exch /ProcSet defineresource pop %%EndResource %%BeingResource: procset unifont 20 dict begin % (utf-8-string) ushow - /ushow { deutf { % c ushow.findglyph { % c /glyph ushow.printglyph % } { % c ushow.substitute } ifelse } forall % } def % Decode utf-8 % % (utf8-string) -> [ codepoint codepoint ... codepoint ] % % in case of malformed string, codepoint -1 is inserted where % the parser failed to interpret data. /deutf { mark exch 0 exch { { % expect c % continuation byte dup 2#11000000 and 2#10000000 eq { % check whether we're in the middle % of sequence 1 index 0 gt { % ok, add this to the last codepoint 2#00111111 and 3 2 roll 6 bitshift or exch 1 sub } { % nope, malformed string pop -1 0 } ifelse exit } if % non-continuation byte while we're in the middle % of sequence 1 index 0 ne { pop -1 0 exit } if % 0-, 1-, ..., 5-seq. starting bytes dup 2#10000000 and 2#00000000 eq { exch exit } if dup 2#11100000 and 2#11000000 eq { 2#00011111 and exch pop 1 exit } if dup 2#11110000 and 2#11100000 eq { 2#00001111 and exch pop 2 exit } if dup 2#11111000 and 2#11110000 eq { 2#00000111 and exch pop 3 exit } if dup 2#11111100 and 2#11111000 eq { 2#00000011 and exch pop 4 exit } if dup 2#11111110 and 2#11111100 eq { 2#00000001 and exch pop 5 exit } if % ignored code -- should not happen, but anyway pop exit } loop } forall % check for incomplete string 0 ne { -1 } if counttomark array astore exch pop } def % Find glyph name for codepoint $uni in current font. % % uni -> uni /glyphname true % uni -> uni false % % What this actually does is making a list of possible names, % say, [ /uni0020 /space /spacehackarabic ], and then trying % each of them against currentfont's CharStrings. /ushow.findglyph { currentfont /CharStrings get false % uni CS F [ 3 index ushow.uniname % uni CS F [ un ReverseGlyphList 5 index .knownget { % uni CS F [ un nns dup type /arraytype eq { aload pop } if % uni CS F [ un n n ... } if ] { % uni CS F name 2 index 1 index known { % uni CS F name exch pop true exit } { pop } ifelse } forall % uni CS name? TF { exch pop true } { pop false } ifelse } def % Fallback glyph name, for characters not in AGL: /uni(code), % with (code) = %04X the actual unicode value. % Sadly this is only a fallback option, since fonts are not required % to define these names for all characters, and more often than not % have /a but not /uni0061. % % 16#431 -> /uni0431 /ushow.uniname { 16 10 string cvrs % (431) dup length 4 le { % (431) dup length 4 exch sub 7 string % (431) 1 (-------) dup 0 (uni) putinterval % (431) 1 (uni----) 1 index 0 gt { % (431) 1 (uni----) 1 1 index 3 exch 1 exch 2 add { 1 index exch (0) putinterval } for } if % (431) 1 (uni0---) dup 4 1 roll 3 1 roll % (uni0---) (uni0---) (431) 1 dup 0 gt { 3 add } { pop 3 } ifelse exch putinterval } { dup length 1 add string % (12345) (- -----) dup 0 (u) putinterval % (12345) (u -----) dup 2 index 1 exch putinterval exch pop } ifelse cvn } def % Show the glyph *and* do stats if necessary. % % code /glyph -> /ushow.printglyph { systemdict /noteunicode .knownget { 2 index exch exec } if exch ushow.substcode 0 gt { glyphshow } { gsave glyphshow grestore } ifelse } def % Well $code is not in currentfont, so got to print notdef instead. % The idea is to have resulting text width close to what it would be % with the glyph available, at least for monospace fonts. % % code -> /ushow.substitute { ushow.substcode { /.notdef glyphshow } repeat } def /ushow.substcode { { 16#0000 16#02FF 1 ushow.rangew % ASCII stuff and generic Latin 16#0300 16#036F 0 ushow.rangew % generic combining stuff 16#20D0 16#20EF 0 ushow.rangew 16#0483 16#0489 0 ushow.rangew 16#0591 16#05A1 0 ushow.rangew 16#1100 16#115F 2 ushow.rangew % Hangul double-width 16#1160 16#11F9 0 ushow.rangew % Hangul combining 16#FFE0 16#FFE6 2 ushow.rangew 16#2E80 16#3098 2 ushow.rangew 16#309D 16#4DB5 2 ushow.rangew 16#4E00 16#9FC3 2 ushow.rangew 16#A000 16#A4C6 2 ushow.rangew 16#0E31 16#0E31 0 ushow.rangew % Thai combining 16#0E34 16#0E3A 0 ushow.rangew % Thai combining 16#0E47 16#0E4E 0 ushow.rangew % Thai combining 16#1D300 16#1D371 2 ushow.rangew 16#1F100 16#1F1FF 2 ushow.rangew % Double-width letters 16#1F030 16#1F061 2 ushow.rangew % Domino horizontal 16#E0000 16#E01FF 2 ushow.rangew pop 1 exit } loop } def % code from to width -> width exit % code from to width -> code /ushow.rangew { 3 index 3 index ge 4 index 3 index le and { exch pop exch pop exch pop exit } { pop pop pop } ifelse } def currentdict end /unifont exch /ProcSet defineresource pop %%EndResource %%BeingResource: procset uniterm 10 dict begin % landscape /la { paper-h 0 translate 90 rotate } def % terminal reset /tr { fR term-ox term-oy moveto } def % low color table (taken from rxvt-unicode init.C def_colorName[]) /colortable [ 16#000000 % 0: black (Black) 16#cd0000 % 1: red (Red3) 16#00cd00 % 2: green (Green3) 16#cdcd00 % 3: yellow (Yellow3) 16#0000cd % 4: blue (Blue3) 16#cd00cd % 5: magenta (Magenta3) 16#00cdcd % 6: cyan (Cyan3) 16#cdcdcd % 7: grey 16#404040 % 8: bright black (Grey25) 16#ff0000 % 1/9: bright red (Reed) 16#00ff00 % 2/10: bright green (Green) 16#ffff00 % 3/11: bright yellow (Yellow) 16#0000ff % 4/12: bright blue (Blue) 16#ff00ff % 5/13: bright magenta (Magenta) 16#00ffff % 6/14: bright cyan (Cyan) 16#ffffff % 7/15: bright white (White) ] def % color cube steps (same source) /colorramp [ 16#00 16#5F 16#87 16#AF 16#D7 16#FF ] def % gray ramp /grayramp [ 16#08 16#12 16#1c 16#26 16#30 16#3a 16#44 16#4e 16#58 16#62 16#6c 16#76 16#80 16#8a 16#94 16#9e 16#a8 16#b2 16#bc 16#c6 16#d0 16#da 16#e4 16#ee ] def % 16#RRGGBB -> - /sethexcolor { % c dup 256 mod 255 div exch % B c 256 idiv % B c' dup 256 mod 255 div exch % B G c' 256 idiv % B G c'' 256 mod 255 div % B G R 3 1 roll exch setrgbcolor } def % i -> 16#RRGGBB /termcolor { 256 mod dup 16 lt { % basic color colortable exch get } { dup 232 ge { % gray ramp 232 sub grayramp exch get dup dup 8 bitshift or 8 bitshift or } { % color cube 16 sub dup 6 mod colorramp exch get % i B exch 6 idiv % B i' dup 6 mod colorramp exch get % B i' G exch 6 idiv % B G i'' 6 mod colorramp exch get % B G R 8 bitshift or 8 bitshift or % 16#RRGGBB } ifelse } ifelse } def % background color (hex), or null to skip background filling /gc null def % current postscript color is used to store fg color % Draw string using term-fg and term-bg % (string) u - /u { gc null ne { gsave currentpoint pop % (s) x0 1 index ushow % (s) x0 currentpoint newpath moveto % (s) x0 gc sethexcolor % (s) x0 0 -0.2 ex mul rmoveto % (s) x0 0 ex rlineto % (s) x0 currentpoint exch pop lineto % (s) 0 ex neg rlineto closepath fill grestore } if ushow } def % end-of-line, finish ul/sl, move onto the next line /n { ux null ne dup { ue } if sx null ne dup { se } if currentpoint exch pop ex sub term-ox exch moveto { sl } if { ul } if } def % underlining /ux null def /ul { currentpoint pop /ux exch def } def /ue { ux null ne { gsave ex 50 div setlinewidth currentpoint ex 8 div sub dup ux exch moveto lineto stroke grestore /ux null def } if } def % strike-out /sx null def /sl { currentpoint pop /sx exch def } def /se { sx null ne { gsave ex 50 div setlinewidth currentpoint ex .25 mul add dup sx exch moveto lineto stroke grestore /sx null def } if } def /rf { color-bg sethexcolor } def /vf { color-bg sethexcolor } def /hf { color-hb sethexcolor } def /nf { color-fg sethexcolor } def /vg { /gc color-bg def } def /rg { /gc color-fg def } def /fg { termcolor sethexcolor } def /bg { termcolor /gc exch def } def /ng { /gc null def } def /cr { currentpoint exch pop term-ox exch moveto } def /bs { em neg 0 rmoveto } def /cc { currentpoint pop term-ox sub em div round cvi } def /t { tabstop dup cc exch mod sub em mul 0 rmoveto } def % black background /bk { 0 fg -1 -1 moveto -1 paper-h 1 add lineto paper-w 1 add paper-h 1 add lineto paper-w 1 add -1 lineto closepath fill } def /fontcmd { 1 index type /nametype eq { dup } { 1 index mul exch } ifelse matrix scale exch findfont exch makefont /setfont load 2 array astore cvx } def /cpt { 100 div } def % centipoints /mil { 1000 div } def % promille currentdict end /uniterm exch /ProcSet defineresource pop %%EndResource %%EndProlog %%BeginSetup /gscompat/ProcSet findresource { def } forall /unidata/ProcSet findresource { def } forall /unifont/ProcSet findresource { def } forall /uniterm/ProcSet findresource { def } forall % Width and height of characters % Ratio is 1:2 exactly /cH 1000 cpt def /cW cH 2 div def % Each character is rendered individually on a grid % using these steps /em cW def % terminal grid x-step /ex cH def % terminal grid y-step /tabstop 8 def /fR /Iosevka-Light cH fontcmd def /fI /Iosevka-Italic cH fontcmd def /fB /Iosevka-Bold cH fontcmd def /fO /Iosevka-Bold-Italic cH fontcmd def % page size /paper-w cW {{ page_width }} mul def /paper-h cH {{ page_height }} mul def /margin-t 0 def /margin-r 0 def /margin-b 0 def /margin-l 0 def % terminal output area corners: x left/middle/right, y top/bottom /term-xl 0 def /term-yb 0 def /term-yt paper-h def % starting position on the page (line 1 col 1 baseline) /term-ox 0 def /term-oy term-yt ex .8 mul sub def % base terminal colors /color-fg 16#000000 def /color-bg 16#FFFFFF def /color-hb 16#AAAAAA def /color-ln 16#AAAAAA def << /PageSize [ paper-w paper-h ] >> setpagedevice %%EndSetup PK!monospace/core/layout.pydef layout(): pass PK!nnmonospace/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_file=source_filename, format="markdown", to="json" ) return json.loads(raw_ast) PK!R,..monospace/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]) elif kind == "Div": return self.process_div(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]]) def process_div(self, value): kind = value[0][1][0] if kind == "Aside": return d.Aside(self.process_elements(value[1])) else: return d.Unprocessed("Div:" + kind) 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!`88monospace/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, AnsiFormatter, FormatTag, Format """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.Aside): blocks.append(self.render_aside(element)) 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 = self.get_subrenderer( main_width=self.settings.main_width - self.settings.tab_size ) # 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 :) def decorate(spaces, n): bullet = "•" offset = 1 if ordered: bullet = styles.circled(n) offset = 2 # Circled numbers are two characters wide... if self.formatter == AnsiFormatter and n <= 20: # ...Unless you print them in a terminal and # the number is <= 20? o_O offset = 1 result = bullet + spaces[offset:] return result for i, elements in enumerate(ordered_list.list_elements): sub_blocks = renderer.render_elements(elements) for j, block in enumerate(sub_blocks): for k, line in enumerate(block.main): indent = " " * self.settings.tab_size if j == 0 and k == 0: indent = decorate(indent, i) formatted_indent = self.formatter.format_tags([indent]) block.main[k] = formatted_indent + 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 ) line_elements = ["━" * self.settings.main_width] formated_line = self.formatter.format_tags(line_elements) lines.insert(0, formated_line) # 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) def render_aside(self, aside): # Note: although aside is composed of multiple blocks, # we want to only return one block, because aside shouldn't be broken lines = [] # Produce this: # ....────────.... # ....(blocks).... # ....────────.... # ^ ^ # \ tab_size / main_width = self.settings.main_width tab_size = self.settings.tab_size f = self.formatter.format_tags width = main_width - 2 * tab_size gray = FormatTag( kind=Format.Color, data={"foreground": "#aaaaaa"} ) left_indent = f([gray, " " * tab_size]) right_indent = f([" " * tab_size, gray.close_tag]) fence = left_indent + f(["─" * width]) + right_indent empty_line = f([" " * main_width]) renderer = self.get_subrenderer(main_width=width) blocks = renderer.render_elements(aside.elements) lines.append(fence) for block in blocks: for line in block.main: lines.append(left_indent + line + right_indent) lines.append(empty_line) lines[-1] = fence # TODO: Notes return b.Block(main=lines) def get_subrenderer(self, main_width=None): return Renderer( settings=replace( self.settings, main_width=( self.settings.main_width if main_width is None else main_width ) ), cross_references=self.cross_references, formatter=self.formatter ) PK!$monospace/core/rendering/__init__.pyPK!\hh%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, Format 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(Format[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 filtered_lines: List[List[Union[FormatTag, str]]] = [ [text_filter(elem) if isinstance(elem, str) else elem for elem in line] for line in lines ] if formatter is not None: return [ formatter.format_tags(line) for line in filtered_lines ] else: return [ "".join(elem for elem in line if isinstance(elem, str)) for line in filtered_lines ] 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.2.dist-info/WHEEL A н#J@Z|Jmqvh&#hڭw!Ѭ"J˫( } %PK!H6O"monospace-0.1.2.dist-info/METADATAUnF):'ɢMPB$E'H$G."E/-҃Phc\}kB~IKqRVog曝0kTKB=V`R,Ak=9ӳUE|rtxBT?ɋj5wh|5,oW ^1]G| ~=!>L"/)=96/)Tb(Y3IޅRPa "ɨjBm};Reng\\.^,?.ϗgN['G{vwcYM'x\'JIcIB?q ~wb<Ā>ɜѡt3C%=]Fe2R!p6X`Xuɮ`ьbaT;"|~^Lmqpeg0 [Ggӆ#DÐPN-s׭ K%wv]~raL9/: *25 ej I 0Sx5_>Vxb)TTp31 TR/]@ /?n:AUҷy7Ga=ZBW|>JK KF@9hDN(*i8wf]ѬdL&% >2Z|ԉ,%!2,#Uwsa 9l74l[p8~,WهnpHߚ~rVK=p&Ip)p};|_u{ 8`&TIy\>>z k߹&t1Fs8&I]r~mXx/0i! ">z{c_mU:/=' E$ 1+MJB[7=lN.åF−D# a&j5̺Woo):#L]=1vj\Nj2hBbΒ8?/uԌ֑rzgM$Hc~Ͷˡb7