def test_replace(): """ Includes testing link creation. """ parts = { 'index': trim(""" Test document - The Simple Test - The More Complex Test """), 'test': trim(""" This is a @[simple] test. This is a @[-more complex] test. """), } outline = Outline(parts, default_counters()) placeholders = CrossReferences(parts, outline) tokenized = placeholders.insert(parts) html_parts = placeholders.replace(tokenized) out = html_parts['test'] assert 'The Simple Test' in out assert '§2' in out # <-- complex uses shorthand assert 'the-simple-test' in out assert 'the-more-complex-test' in out
def test_constructor(): parts = sample_doc() outline = Outline(parts, default_counters()) footnotes = Footnotes(parts, outline, 'prefix_') assert footnotes.outline.errors == { 'topic-four': [('', 'More <kbd>^[link]s</kbd> than footnotes! (+1)')], 'topic-three': [('', 'More footnotes than <kbd>^[link]s</kbd>! (+1)')] }
def test_find_numbering(): arg = { 'index': trim(""" Title - Chapter One - - Chapter One Aye - Chapter Two """), 'chapter-one': "Chapter One\n\n" + ('word ' * 998) } outline = Outline(arg, default_counters()) assert outline.find_numbering('chapter-one') == ['1'] assert outline.find_numbering('chapter-one-aye') == ['1', 'a'] assert outline.find_numbering('chapter-two') == ['2']
def test_outline(): arg = { 'index': trim(""" Title - Chapter One - Chapter Two """), 'chapter-one': "Chapter One\n\n" + ('word ' * 998) } outline = Outline(arg, default_counters()) out = outline.html('/') # <-- edit_base_uri # @todo: Check this. # dom = html.fragment_fromstring(out, create_parent='body')[0] # assert len(dom.cssselect('tr')) == 4 rows = findall('<tr', out) assert len(rows) == 4
def test_elements(): arg = { 'blog-1': "Blog One\n\n" + ('word ' * 500), 'blog-2': "Blog Two\n\n" + ('word ' * 500) } outline = Outline(arg, default_counters()) # Numbering, slug, title, title_slug, word count. expected = [(['1'], 'blog-1', 'Blog One', 'blog-one', 502), (['2'], 'blog-2', 'Blog Two', 'blog-two', 502)] assert outline.elements == expected
def test_get_count(): parts = { 'index': 'INDEX', 'other': 'OTHER', } outline = Outline(parts, default_counters()) index = Index(outline) assert index.get_count('index') == 'i' assert index.get_count('index') == 'ii' assert index.get_count('other') == 'i' assert index.get_count('other') == 'ii'
def test_tag(): parts = {} outline = Outline(parts, default_counters()) index = Index(outline) link = index.tag('alias', 'tag', 'subtag', '?', ['1', 'a'], 'i') assert '<a id="tag_subtag_1.a_i" href="#ref_tag_subtag_1.a_i">' in link assert 'alias?<sup>i</sup>' in link # <-- (?) assert '</a>' in link back_link = '<a id="ref_tag_subtag_1.a_i" href="#tag_subtag_1.a_i">' assert back_link in index.tags['tag']['subtag']['1.a'][0]
def test_footnotes(): """ Simple count that we have the right number of footnotes. """ parts = sample_doc() footnotes = Footnotes(parts, Outline(parts, default_counters()), id_prefix) links = Links(footnotes) new_parts = links.insert(parts) end_parts = links.replace(new_parts) out = footnotes.html() dom = html.fragment_fromstring(out, create_parent='body')[0] assert len(dom.cssselect('sup')) == 6
def test_get_count(): parts = { 'index': 'INDEX', 'other': 'OTHER', } outline = Outline(parts, default_counters()) bibliography = Bibliography(parts, outline, id_prefix) assert bibliography.get_count('index') == '*' assert bibliography.get_count('index') == '†' assert bibliography.get_count('index') == '‡' assert bibliography.get_count('other') == 'a' assert bibliography.get_count('other') == 'b' assert bibliography.get_count('other') == 'c'
def test_match(): parts = { 'biblio': trim(""" Author. 1999. Title #1. Publisher, City. Author. 2000. Title #1. Publisher, City. Author. 2000. Title #2. Publisher, City. """), } outline = Outline(parts, default_counters()) bibliography = Bibliography(parts, outline, id_prefix) assert bibliography.match('author') == "Author. 1999." assert bibliography.match('author 2000') == "Author. 2000a."
def test_html_simplest(): parts = { 'biblio': trim(""" Author. 1999. Title #1. Publisher, City. Author. 2000. Title #1. Publisher, City. Author. 2000. Title #2. Publisher, City. """), } outline = Outline(parts, default_counters()) bibliography = Bibliography(parts, outline, id_prefix) out = bibliography.html() dom = html.fragment_fromstring(out, create_parent='body')[0] assert len(dom.cssselect('div.indent-hanging')) == 3
def test_citation(): parts = {} outline = Outline(parts, default_counters()) bibliography = Bibliography(parts, outline, id_prefix) link = bibliography.citation('Author /Title/', 'p.34', '.', 'Author. 2000.', ['1', 'a'], 3) assert '<a id="PREFIX_author-2000_1.a_3" ' + \ 'href="#ref_PREFIX_author-2000_1.a_3">' in link assert '(Author <em>Title</em>, p.34).' in link # <-- (?) assert '<sup>3</sup>' in link assert '</a>' in link back_link = '<a id="ref_PREFIX_author-2000_1.a_3" ' + \ 'href="#PREFIX_author-2000_1.a_3">' assert back_link in bibliography.citations['Author. 2000.']['1.a'][0]
def test_constructor(): parts = { 'index': trim(""" My Document! It contains a reference to ~[Chapman, /Conversation/, p.34]. """), 'biblio': trim(""" Chapman, Nigel. 2014. Private conversation. Chapman, Nigel. 2014. Private conversation. #2. """), } outline = Outline(parts, default_counters()) bibliography = Bibliography(parts, outline, id_prefix) citations = Citations(bibliography) assert bibliography.entries == SortedDict({ "Chapman, Nigel. 2014a.": "Private conversation.", "Chapman, Nigel. 2014b.": "Private conversation. #2.", }) new_parts = citations.insert(parts) assert new_parts == { 'index': trim(""" My Document! It contains a reference to %scitation:1%s """) % (DELIMITER, DELIMITER), # ^ note the missing fullstop 'biblio': trim(""" Chapman, Nigel. 2014. Private conversation. Chapman, Nigel. 2014. Private conversation. #2. """), } end_parts = citations.replace(new_parts) assert '<em>Conversation</em>' in end_parts['index'] assert 'p.34' in end_parts['index'] dom = html.fragment_fromstring(end_parts['index'], create_parent='body')[0] assert len(dom.cssselect('a')) == 1
def test_constructor(): parts = { 'index': trim(""" My Document! It contains a #[Tag] and a %[Tag]. ` Part One """), 'part-one': trim(""" Or an #[alias: Tag, subtag]? """), } outline = Outline(parts, default_counters()) index = Index(outline) tags = Tags(index) new_parts = tags.insert(parts) assert new_parts == { 'index': trim(""" My Document! It contains a %stag:1%s and a %stag:2%s ` Part One """) % (DELIMITER, DELIMITER, DELIMITER, DELIMITER), 'part-one': trim(""" Or an %stag:1%s """) % (DELIMITER, DELIMITER), } end_parts = tags.replace(new_parts) assert 'Tag<sup>i</sup>' in end_parts['index'] assert 'Tag.<sup>ii</sup>' in end_parts['index']
def test_html_simplest(): parts = { 'index': trim(""" My Document! It contains a #[Tag] and a %[Tag]. ` Part One """), 'part-one': trim(""" Or an #[alias: Tag, subtag]? """), } outline = Outline(parts, default_counters()) index = Index(outline) index.tags = {'tag': {'subtag': {'1.1': ['LINK']}}} out = index.html() dom = html.fragment_fromstring(out, create_parent='body')[0] assert len(dom.cssselect('div.indent-first-line')) == 1 assert 'LINK' in out
class Wiki(object): """ The simple block-based Article Wiki parser, designed for academic writing. """ __version__ = '0.1' def __init__(self, settings=None): """ Settings hold all necessary context information. """ assert isinstance(settings, Settings) or settings is None self.settings = settings self.html = Html(self.settings) self.id_prefix = self.settings.get( 'config:document', random_slug('wiki_') # <-- else ) self.outline = None self.cross_references = None self.footnotes = None self.links = None self.index = None self.tags = None self.bibliography = None self.citations = None def process(self, user_slug, doc_slug, parts_dict, fragment=False, preview=False): """ Generate a complete HTML document from a dictionary. - To process a directory, supply a dictionary with no 'index' key. - To process a document, supply a dictionary with an index entry. - To process an index _only_, supply {'index': text}. - To process a section, supply {slug: text} when slug != 'index'. - To process a fragment, without a headline, supply {slug: text} with fragment=true (used in DEMO blocks). """ if len(parts_dict) == 0: return ValueError("Document is empty.") validate_document(parts_dict, fragment) parts, files = clean_document(parts_dict) # ------------------------------------------------------ # Add placeholders for elements not processed by the wiki # ------------------------------------------------------- self.demo = Demo() self.backslashes = Backslashes() self.entities = Entities() self.verbatim = Verbatim() # Demo is first: entities(self.backslashes(verbatim(demo))) parts = pipe( [self.entities, self.backslashes, self.verbatim, self.demo], 'insert', parts) self.settings.extract(parts) # @todo:decide on file support. self.settings.set_config('files', files) # @[Cross Reference] self.outline = Outline(parts, default_counters()) self.cross_references = CrossReferences(parts, self.outline) # ^[marker] # ^ Reference self.footnotes = Footnotes(parts, self.outline, self.id_prefix) self.links = Links(self.footnotes, self.id_prefix) # #[Topic, sub-topic] # self.index = Index(self.outline) # self.tags = Tags(self.index) # ~[Author 2000, p.34] self.bibliography = Bibliography(parts, self.outline, self.id_prefix) self.citations = Citations(self.bibliography) parts = pipe( [ self.cross_references, # <-- call 'insert(parts)' self.links, # self.tags, self.citations ], 'insert', parts) # ------------- # Generate HTML # ------------- html_parts = {} # if len(parts) == 1 or fragment: # number = 1 # for slug in sorted(parts): # section = self.make_section( # [str(number)], slug, parts[slug], fragment, preview # ) # number = + 1 # html_parts[slug] = section # else: if 'index' in parts: _, title, _, summary = get_title_data(parts['index'], 'index') self.settings.set('TITLE', title) self.settings.set('SUMMARY', summary) html_parts['index'] = self.make_index(parts['index']) html_parts['index'] += web_buttons(user_slug, doc_slug) if not self.outline.single_page(): edit_base_uri = self.settings.get_base_uri('edit') html_parts['index'] += self.outline.html(edit_base_uri) html_parts['index'] += self.outline.html_spare_parts( parts, edit_base_uri) else: self.settings.set('TITLE', '') for (numbering, slug, _, _, _) in self.outline: if slug in parts and slug not in ['index', 'biblio']: # print u"MAKE SECTION" section = self.make_section(numbering, slug, parts[slug], fragment, preview) html_parts[slug] = section # ------------------------------------- # Replace placeholder content (as HTML) # ------------------------------------- html_parts = pipe( [ self.cross_references, # self.tags, self.links, self.citations ], 'replace', html_parts) html_parts = pipe( [self.demo, self.verbatim, self.backslashes, self.entities], 'replace', html_parts) footnote_parts = self.footnotes.html_parts() footnote_parts = pipe([self.verbatim, self.backslashes, self.entities], 'replace', footnote_parts) html = '<article>\n' html += html_parts['index'] if 'index' in html_parts else '' for (numbering, slug, _, _, _) in self.outline: if slug in parts and slug not in ['index', 'biblio']: html += html_parts[slug] footnote_html = footnote_parts.get(slug) if footnote_html: html += "<footer>" html += "<hr class=\"div-left div-solid\" />" html += footnote_html html += "</footer>" html += self.make_footer() html += '</article>' return html def make_footer(self): """ <footer> <section id="prefix-footnotes"> <section id="prefix-bibliography"> <section id="prefix-index"> """ endmatter = [ _ for _ in [ # self.footnotes.html(), self.bibliography.html(), # self.index.html(), ] if _.strip() != "" ] env = Environment(autoescape=True) tpl = env.from_string( trim(""" <footer> {% if endmatter|length > 0 %} <hr class="div-left div-solid"/> {% for html in endmatter %} {{ html | safe }} {% endfor %} {% endif %} </footer> """)) return tpl.render(endmatter=endmatter, biblio_link=self.settings.get_base_uri('edit') + '/biblio') def dump(self): """ Diagnostics 101 """ print(pformat(self.outline)) def compile_metadata(self, tz_name, user_slug, doc_slug=None): """ Return just the elements we want to store in the document metadata cache. Self.metadata is a copy of settings made after processing the index part, plus other items like count_words. """ data = {} data['user'] = user_slug data['title'] = self.settings.get('TITLE', '') data['slug'] = doc_slug if doc_slug else slugify(data['title']) data['summary'] = self.settings.get('SUMMARY', '') data['license'] = self.settings.get('LICENSE', '') data['publish'] = self.settings.get('PUBLISH', 'YES') # <-- Default data['author'] = self.settings.get('AUTHOR', '') data['email'] = self.settings.get('EMAIL', '') data['facebook'] = self.settings.get('FACEBOOK', '') data['twitter'] = self.settings.get('TWITTER', '') default = date.today().strftime("%d %b %Y") data['date'] = self.settings.get('DATE', default) utc = parse_date(data['date'], tz_name) data['published_time'] = format_date(utc, tz_name, DATE_FORMAT_ISO8601) data['todo'] = self.settings.get('TODO', '') data['word_count'] = self.outline.total_word_count() return data def make_index(self, text): """ Front matter preceding index text. Multi-author blocks are disabled for now; v.0.1.0 is SINGLE_USER. """ env = Environment(autoescape=True) tpl = env.from_string( trim(""" <header> {% if title_html != "" %} <hgroup> <h1 class="balance-text">{{ title_html|safe }}</h1> {% if summary != "" %} <h2 class="balance-text">{{ summary_html|safe }}</h2> {% endif %} </hgroup> {% endif %} {% if author != "" or email != "" %} <div class="author-list"> <address> {% if author != "" %} <div>{{ author }}</div> {% endif %} {% if email != "" %} <div><a href="mailto:{{ email }}">{{ email }}</a></div> {% endif %} </address> </div> {% endif %} {% if facebook != "" or twitter != "" %} <p class="space space-between"> {% if facebook != "" %} <a href="https://facebook.com/{{ facebook }}" target="_blank"> FB: {{ facebook }} </a> {% endif %} {% if twitter != "" %} <a href="https://twitter.com/{{ twitter }}" target="_blank"> TW: {{ twitter }} </a> {% endif %} </p> {% endif %} {% if date %} <p class="space" rel="date"> {% if parsed_date != None %} <time pubdate datetime="{{ parsed_date }}">{{ date }}</time> {% else %} {{ date }} {% endif %} </p> {% endif %} </header> <section class="depth-0"> {{ content_html|safe }} </section> """)) inline = Inline() content, _ = split_bibliography(text) blocks = BlockList(content) title, summary = blocks.pop_titles() content_html = blocks.html(['0'], 'index', self.settings, fragment=True) date_string = inline.process(self.settings.get('DATE', '')) try: dt = parse(date_string) date_yyyymmdd = dt.date().isoformat() except ValueError: date_yyyymmdd = None # author = self.split_author() return tpl.render( title_html=inline.process(title), summary_html=inline.process(summary), author=self.settings.get('AUTHOR', ''), email=self.settings.get('EMAIL', ''), facebook=self.settings.get('FACEBOOK', ''), twitter=self.settings.get('TWITTER', ''), date=date_string, parsed_date=date_yyyymmdd, edit_link=self.settings.get_base_uri('edit') + '/index', content_html=content_html, ) # Move to geometry? def split_author(self, author): """ UNUSED FOR NOW. v.0.1.0 is SINGLE_USER. Split into authors and lines. $ AUTHOR = Author / Affiliation + Author2 / Affiliation2 No author should return an empty list. """ inline = Inline() if author.strip() == '': return [] else: return [[ inline.process(line.strip()) for line in block.split(' / ') ] for block in author.split(' + ')] # Move to geometry? def author_cols(self, author): """ Return twelve-column-grid spans based on numbers. @todo: Fix the hackish; better solution would find the numbers for each row, e.g. 5 would give 3 + 2. @todo: Should be solved with Flexbox now. """ _ = len(author) # list columns = {1: 12, 2: 6, 3: 4, 4: 6} return columns[_] if _ in columns else 4 def make_section(self, numbering, slug, text, fragment=False, preview=False): """ Wrap section HTML in titles and nav. A fragment has no heading. A preview has a heading but no context,so no numbering. """ env = Environment(autoescape=True) tpl = env.from_string( trim(""" <section class="body depth-{{ depth }}"> {% if not fragment %} <nav class="button-list button-list-edge no-preview no-print"> <a class="button" href="{{ edit_link }}"> <i class="fa fa-pencil"></i> Edit </a> </nav> {% endif %} {{ content_html|safe }} </section> """)) content, _ = split_bibliography(text) blocks = BlockList(content) content_html = blocks.html(numbering, slug, self.settings, fragment, preview) return tpl.render(depth=len(numbering), edit_link=self.settings.get_base_uri('edit') + '/' + slug, content_html=content_html, fragment=fragment)
def process(self, user_slug, doc_slug, parts_dict, fragment=False, preview=False): """ Generate a complete HTML document from a dictionary. - To process a directory, supply a dictionary with no 'index' key. - To process a document, supply a dictionary with an index entry. - To process an index _only_, supply {'index': text}. - To process a section, supply {slug: text} when slug != 'index'. - To process a fragment, without a headline, supply {slug: text} with fragment=true (used in DEMO blocks). """ if len(parts_dict) == 0: return ValueError("Document is empty.") validate_document(parts_dict, fragment) parts, files = clean_document(parts_dict) # ------------------------------------------------------ # Add placeholders for elements not processed by the wiki # ------------------------------------------------------- self.demo = Demo() self.backslashes = Backslashes() self.entities = Entities() self.verbatim = Verbatim() # Demo is first: entities(self.backslashes(verbatim(demo))) parts = pipe( [self.entities, self.backslashes, self.verbatim, self.demo], 'insert', parts) self.settings.extract(parts) # @todo:decide on file support. self.settings.set_config('files', files) # @[Cross Reference] self.outline = Outline(parts, default_counters()) self.cross_references = CrossReferences(parts, self.outline) # ^[marker] # ^ Reference self.footnotes = Footnotes(parts, self.outline, self.id_prefix) self.links = Links(self.footnotes, self.id_prefix) # #[Topic, sub-topic] # self.index = Index(self.outline) # self.tags = Tags(self.index) # ~[Author 2000, p.34] self.bibliography = Bibliography(parts, self.outline, self.id_prefix) self.citations = Citations(self.bibliography) parts = pipe( [ self.cross_references, # <-- call 'insert(parts)' self.links, # self.tags, self.citations ], 'insert', parts) # ------------- # Generate HTML # ------------- html_parts = {} # if len(parts) == 1 or fragment: # number = 1 # for slug in sorted(parts): # section = self.make_section( # [str(number)], slug, parts[slug], fragment, preview # ) # number = + 1 # html_parts[slug] = section # else: if 'index' in parts: _, title, _, summary = get_title_data(parts['index'], 'index') self.settings.set('TITLE', title) self.settings.set('SUMMARY', summary) html_parts['index'] = self.make_index(parts['index']) html_parts['index'] += web_buttons(user_slug, doc_slug) if not self.outline.single_page(): edit_base_uri = self.settings.get_base_uri('edit') html_parts['index'] += self.outline.html(edit_base_uri) html_parts['index'] += self.outline.html_spare_parts( parts, edit_base_uri) else: self.settings.set('TITLE', '') for (numbering, slug, _, _, _) in self.outline: if slug in parts and slug not in ['index', 'biblio']: # print u"MAKE SECTION" section = self.make_section(numbering, slug, parts[slug], fragment, preview) html_parts[slug] = section # ------------------------------------- # Replace placeholder content (as HTML) # ------------------------------------- html_parts = pipe( [ self.cross_references, # self.tags, self.links, self.citations ], 'replace', html_parts) html_parts = pipe( [self.demo, self.verbatim, self.backslashes, self.entities], 'replace', html_parts) footnote_parts = self.footnotes.html_parts() footnote_parts = pipe([self.verbatim, self.backslashes, self.entities], 'replace', footnote_parts) html = '<article>\n' html += html_parts['index'] if 'index' in html_parts else '' for (numbering, slug, _, _, _) in self.outline: if slug in parts and slug not in ['index', 'biblio']: html += html_parts[slug] footnote_html = footnote_parts.get(slug) if footnote_html: html += "<footer>" html += "<hr class=\"div-left div-solid\" />" html += footnote_html html += "</footer>" html += self.make_footer() html += '</article>' return html