├── example_issue ├── theotherone │ ├── 1.txt │ └── 2.txt ├── dolly.txt ├── testtest │ ├── 1.txt │ └── 2.txt └── index.json ├── requirements.txt ├── .gitignore ├── .gitmodules ├── Dockerfile ├── dialazine ├── lib │ ├── common_tools.py │ ├── text_screen_reader.py │ ├── zine_functions.py │ ├── contents_reader.py │ └── html_generator.py ├── server.py ├── generate_html.py └── tools │ ├── proof_ascii.js │ └── proof_ascii.html ├── default_templates ├── intro.html ├── main_sample.css ├── story_index.html └── story_page.html ├── README.md └── LICENSE /example_issue/theotherone/1.txt: -------------------------------------------------------------------------------- 1 | Here's another -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | telnetlib3==1.0.4 2 | chevron==0.14.0 3 | -------------------------------------------------------------------------------- /example_issue/theotherone/2.txt: -------------------------------------------------------------------------------- 1 | What do you think of this one? -------------------------------------------------------------------------------- /example_issue/dolly.txt: -------------------------------------------------------------------------------- 1 | This is the introduction to the zine press enter to go to the index -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | .venv/ 3 | __pycache__/ 4 | issue* 5 | dial_a_zine_issue* 6 | newsession_issue2 7 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "issue1"] 2 | path = issue1 3 | url = git@github.com:caraesten/dial_a_zine_issue1.git 4 | -------------------------------------------------------------------------------- /example_issue/testtest/1.txt: -------------------------------------------------------------------------------- 1 | 2 | tsdf 3 | asdfaisdf 4 | 5 | lorem ipsum 6 | wah wah whah wah wha 7 | asdkfja ;sf 8 | asdfo askdjfl ads 9 | fj alksdfj asd 10 | f asdkfj asd 11 | faoisdk fjasd 12 | f asdklfja lskdfj asd 13 | fj asdkfja lsdf 14 | jasd flkasdj faosdf 15 | asdkjfh aslkdfja 16 | sdfj kajsdfj asdhf iasdfhis is my story -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | 3 | ARG ISSUE_DIR=issue1 4 | 5 | FROM python:3.8-slim-buster 6 | 7 | WORKDIR /app 8 | 9 | COPY requirements.txt requirements.txt 10 | RUN pip3 install -r requirements.txt 11 | 12 | COPY dialazine dialazine 13 | COPY $ISSUE_DIR $ISSUE_DIR 14 | 15 | EXPOSE 23/tcp 16 | 17 | CMD [ "python3", "dialazine/server.py" ] 18 | -------------------------------------------------------------------------------- /example_issue/index.json: -------------------------------------------------------------------------------- 1 | { 2 | "hello": "dolly.txt", 3 | "contents": [ 4 | { 5 | "title": "blah", 6 | "author": "me", 7 | "directory": "testtest" 8 | }, 9 | { 10 | "title": "another one", 11 | "author": "also me", 12 | "directory": "theotherone" 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /dialazine/lib/common_tools.py: -------------------------------------------------------------------------------- 1 | 2 | def index_header(include_linebreaks=True): 3 | linebreak_char = '' 4 | if include_linebreaks: 5 | linebreak_char = '\n' 6 | return linebreak_char + " [ INDEX ] " + linebreak_char 7 | 8 | def index_item_string(option, title, author, include_linebreaks=True): 9 | linebreak_char = '' 10 | if include_linebreaks: 11 | linebreak_char = '\n' 12 | return linebreak_char + ("%s > %s < ...by %s" % (option, title, author)) + linebreak_char 13 | -------------------------------------------------------------------------------- /default_templates/intro.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 |
9 | {{#intro_lines}} 10 | {{{.}}}
11 | {{/intro_lines}} 12 |
13 | 16 |
17 | 18 | 19 | -------------------------------------------------------------------------------- /dialazine/lib/text_screen_reader.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | class TextScreenReader: 4 | def __init__(self, root_directory): 5 | self.root_directory = root_directory 6 | def read_file_name(self, path): 7 | full_path = "%s/%s" % (self.root_directory, path) 8 | with open(full_path, 'r') as f: 9 | full_file = f.readlines() 10 | f.close() 11 | return full_file 12 | def does_file_exist(self, file_path): 13 | return os.path.exists("%s/%s" % (self.root_directory, file_path)) 14 | -------------------------------------------------------------------------------- /dialazine/server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import asyncio, telnetlib3 3 | import os 4 | import pathlib 5 | from lib.zine_functions import ZineFunctions 6 | 7 | LOCALHOST_PORT = 23 8 | CONTENT_FOLDER = "example_issue" 9 | 10 | async def shell(reader, writer): 11 | root_dir_path = pathlib.Path(__file__).parent.parent.absolute() 12 | 13 | zine = ZineFunctions(reader, writer, "%s/%s" % (root_dir_path.as_posix(), f"{CONTENT_FOLDER}/index.json")) 14 | await zine.run_index() 15 | 16 | loop = asyncio.get_event_loop() 17 | srv = telnetlib3.create_server(port=LOCALHOST_PORT, shell=shell, timeout=3600) 18 | server = loop.run_until_complete(srv) 19 | loop.run_until_complete(server.wait_closed()) 20 | -------------------------------------------------------------------------------- /default_templates/main_sample.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: monospace; 3 | background-color: black; 4 | color: #39ff14; 5 | } 6 | 7 | a { 8 | color: #ddd; 9 | } 10 | 11 | h2 { 12 | text-align: center; 13 | } 14 | 15 | .main { 16 | width: 82ch; /* fudge it a bit */ 17 | margin-left: auto; 18 | margin-right: auto; 19 | } 20 | 21 | .viewport { 22 | height: 30em; 23 | margin-top: 2em; 24 | } 25 | 26 | .stories-list li { 27 | margin: 0.5em; 28 | } 29 | 30 | .navigation { 31 | margin-top: 2em; 32 | text-align: center; 33 | } 34 | 35 | .navigation a { 36 | display: inline-block; 37 | } 38 | 39 | .about { 40 | color: #1c830a; 41 | text-align: center; 42 | font-size: 10pt; 43 | margin-top: 1em; 44 | } 45 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## How to run 2 | 3 | To run locally, run server.py as a python file e.g. from the root directory 4 | 5 | `python3 dialazine/server.py` 6 | 7 | To access locally, run 8 | 9 | `telnet localhost 23` 10 | 11 | (or equivalent with another telnet client) 12 | 13 | in server.py, change CONTENT_FOLDER to "example_issue" to see example 14 | 15 | ## Zine Structure 16 | 17 | In the index.json, the "hello" message is the filepath within the issue folder for what is first displayed, 18 | 19 | Then the contents is a list with each article, taking the "title" and "author" for the index, and the "directory" is the folder within the issue folder that contains the article pages - the pages must be called `1.txt`, `2.txt` etc. and will automatically display in that order 20 | -------------------------------------------------------------------------------- /default_templates/story_index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 |
9 |

{{header}}

10 | 15 |
16 | 19 |
20 | Sample zine, generated by dial-a-zine telnet cms! 21 |
22 |
23 | 24 | 25 | -------------------------------------------------------------------------------- /dialazine/generate_html.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | import os 3 | import argparse 4 | from pathlib import Path 5 | from lib.html_generator import HtmlGenerator 6 | 7 | TEMPLATES = "default_templates" 8 | CONTENT_FOLDER = "example_issue" 9 | 10 | parser = argparse.ArgumentParser(description="Use this tool to export html documents for your zine") 11 | parser.add_argument("outputdir", type=Path, help="output directory for generated HTML") 12 | 13 | args = parser.parse_args() 14 | 15 | if not args.outputdir: 16 | raise ValueError("Required param outputdir missing, run program with -h") 17 | 18 | html_directory = args.outputdir.as_posix() 19 | 20 | root_dir_path = Path(__file__).parent.parent.absolute() 21 | index_directory = "%s/%s" % (root_dir_path.as_posix(), f"{CONTENT_FOLDER}/index.json") 22 | templates_directory = "%s/%s" % (root_dir_path.as_posix(), TEMPLATES) 23 | generator = HtmlGenerator(index_directory, html_directory, templates_directory) 24 | generator.write_zine_html() -------------------------------------------------------------------------------- /default_templates/story_page.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 |
9 | {{#story_lines}} 10 | {{{.}}}
11 | {{/story_lines}} 12 |
13 | 28 |
29 | {{story_title}} by {{story_author}}, page {{page_number}} 30 |
31 |
32 | 33 | 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Cara Esten Hurtle 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /dialazine/tools/proof_ascii.js: -------------------------------------------------------------------------------- 1 | class AsciiScanner { 2 | static ASCII_REGEX = /([\u{00e1}-\u{FFFF}]+)/gu; 3 | constructor(inputElement, matchesOutput, textOutput) { 4 | this.inputElement = inputElement; 5 | this.matchesOutput = matchesOutput; 6 | this.textOutput = textOutput; 7 | } 8 | 9 | start() { 10 | this.inputElement.addEventListener('input', (event) => { 11 | const text = event.target.value; 12 | this.onTextChange(text); 13 | }) 14 | } 15 | 16 | onTextChange(newText) { 17 | const output = newText.replace(AsciiScanner.ASCII_REGEX, (match) => { 18 | return `${match}`; 19 | }); 20 | this.textOutput.innerHTML = output; 21 | 22 | const errors = ((newText || '').match(AsciiScanner.ASCII_REGEX) || []).length; 23 | this.matchesOutput.innerHTML = `Found: ${errors} non-ASCII blocks` 24 | } 25 | } 26 | 27 | window.onload = function() { 28 | const scanner = new AsciiScanner( 29 | document.getElementById('text-input'), 30 | document.getElementById('matches-output'), 31 | document.getElementById('text-output')); 32 | 33 | scanner.start(); 34 | } 35 | -------------------------------------------------------------------------------- /dialazine/tools/proof_ascii.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ASCII Scanner 5 | 6 | 43 | 44 | 45 |
46 |

ASCII Scanner

47 |

Paste Text Below

48 | 49 |
50 |
51 |
52 | 53 | -------------------------------------------------------------------------------- /example_issue/testtest/2.txt: -------------------------------------------------------------------------------- 1 | mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm 2 | mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm 3 | mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmommmmmmmmmmmmmmmmmmm 4 | mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm 5 | mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm 6 | mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm 7 | mmmmmmmmmmmmmmmmmmmmmmmmmmmmommmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm 8 | mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm 9 | mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmommmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm 10 | mmmmmmmmommmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm 11 | mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm 12 | mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmommmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm 13 | mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm 14 | mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm 15 | mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm 16 | mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm 17 | mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmommmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm 18 | mmmmmmmmmmommmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm 19 | mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm 20 | mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm 21 | mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm 22 | mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm 23 | mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm 24 | mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm -------------------------------------------------------------------------------- /dialazine/lib/zine_functions.py: -------------------------------------------------------------------------------- 1 | from lib.contents_reader import ContentsReader 2 | import asyncio 3 | 4 | CLEAR_SCREEN = "\u001b[2J" 5 | NEW_LINE = "\r\n" 6 | 7 | class ZineFunctions: 8 | def __init__(self, reader, writer, index_file_path): 9 | self.reader = reader 10 | self.writer = writer 11 | self.contents_reader = ContentsReader(index_file_path) 12 | 13 | async def run_index(self): 14 | for welcome_line in self.contents_reader.read_hello_file(): 15 | self.writer.write(welcome_line) 16 | await self.writer.drain() 17 | # Read one byte (any key) 18 | await self.reader.read(1) 19 | running = True 20 | while (running): 21 | for index_line in self.contents_reader.read_index_lines(): 22 | self.writer.write(index_line) 23 | item_choice = await self.reader.read(1) 24 | item_choice_int = -1 25 | if item_choice.upper() == 'X': 26 | running = False 27 | continue 28 | item_choice_int = self.contents_reader.map_input_to_numerical_index(item_choice) 29 | if item_choice_int == -1: 30 | self.writer.write(f"{NEW_LINE}{NEW_LINE}Pick a story, or X to quit.{NEW_LINE}") 31 | continue 32 | self.writer.write(f"{NEW_LINE}{NEW_LINE}...you picked: %s" % (item_choice)) 33 | self.writer.write(f"{NEW_LINE}{NEW_LINE}...press RETURN to start reading, and to continue after each page") 34 | await self.reader.read(1) 35 | self.writer.write(NEW_LINE + CLEAR_SCREEN) 36 | await asyncio.sleep(1) 37 | await self.run_story(item_choice_int) 38 | self.disconnect() 39 | 40 | async def run_story(self, story_number): 41 | page_number = 1 42 | 43 | story_lines = self.contents_reader.read_story(story_number, page_number) 44 | while len(story_lines) > 0: 45 | self.writer.write(CLEAR_SCREEN) 46 | for story_line in story_lines: 47 | self.writer.write(story_line) 48 | await self.writer.drain() 49 | char_read = await self.reader.readline() 50 | page_number += 1 51 | story_lines = self.contents_reader.read_story(story_number, page_number) 52 | 53 | def disconnect(self): 54 | self.writer.close() 55 | -------------------------------------------------------------------------------- /dialazine/lib/contents_reader.py: -------------------------------------------------------------------------------- 1 | import json, os, string 2 | from lib.common_tools import index_item_string 3 | from lib.text_screen_reader import TextScreenReader 4 | from lib.common_tools import index_header 5 | 6 | class ContentsReader: 7 | def __init__(self, contents_file_path): 8 | self.issue_directory = os.path.dirname(contents_file_path) 9 | self.text_reader = TextScreenReader(self.issue_directory) 10 | contents_file = open(contents_file_path, 'r') 11 | self.contents_json = json.load(contents_file) 12 | self.verify_contents() 13 | 14 | def verify_contents(self): 15 | if not self.contents_json['hello']: 16 | raise Exception("Contents JSON requires a hello message") 17 | 18 | def hello_file_path(self): 19 | return self.contents_json['hello'] 20 | 21 | def read_hello_file(self, wrap_returns=True): 22 | lines = self.text_reader.read_file_name(self.hello_file_path()) 23 | if wrap_returns: 24 | return self._wrap_carriage_returns(lines) 25 | else: 26 | return lines 27 | 28 | def read_index_lines(self): 29 | index_lines = [index_header()] 30 | option_number = 1 31 | for index_item in self.read_story_object(): 32 | index_lines.append(index_item_string(self._index_to_option(option_number), index_item['title'], index_item['author'])) 33 | option_number += 1 34 | index_lines.append("\n(or X to quit!)\n") 35 | return self._wrap_carriage_returns(index_lines) 36 | 37 | def read_story_object(self, include_contents=False): 38 | index_list = [] 39 | story_number = 1 40 | for index_item in self.contents_json['contents']: 41 | story_item = { 42 | 'title': index_item['title'], 43 | 'author': index_item['author'], 44 | 'directory': index_item['directory'], 45 | } 46 | if include_contents: 47 | page_number = 1 48 | story_lines = self.read_story(story_number, page_number, wrap_returns=False) 49 | pages = [] 50 | while len(story_lines) > 0: 51 | pages.append(story_lines) 52 | page_number += 1 53 | story_lines = self.read_story(story_number, page_number, wrap_returns=False) 54 | story_item['contents'] = pages 55 | story_number += 1 56 | index_list.append(story_item) 57 | return index_list 58 | 59 | def read_story(self, story_number, page = 1, wrap_returns=True): 60 | if story_number > len(self.contents_json['contents']): 61 | return [] 62 | story_obj = self.contents_json['contents'][story_number - 1] 63 | file_path = "%s/%s.txt" % (story_obj['directory'], page) 64 | 65 | if self.text_reader.does_file_exist(file_path): 66 | page_lines = self.text_reader.read_file_name(file_path) 67 | if wrap_returns: 68 | return self._wrap_carriage_returns(page_lines) 69 | else: 70 | return page_lines 71 | else: 72 | return [] 73 | 74 | def _wrap_carriage_returns(self, lines_list): 75 | return [x + '\r' for x in lines_list] 76 | 77 | def map_input_to_numerical_index(self, input_string): 78 | try: 79 | return int(input_string) 80 | except ValueError: 81 | pass 82 | try: 83 | return 10 + string.ascii_uppercase[0:10].index(input_string) 84 | except ValueError: 85 | return -1 86 | 87 | def _index_to_option(self, input_index): 88 | if (input_index < 10): 89 | return input_index 90 | else: 91 | return string.ascii_uppercase[input_index - 10] 92 | 93 | -------------------------------------------------------------------------------- /dialazine/lib/html_generator.py: -------------------------------------------------------------------------------- 1 | import chevron 2 | from pathlib import Path 3 | from lib.common_tools import index_item_string, index_header 4 | from lib.contents_reader import ContentsReader 5 | 6 | class HtmlGenerator: 7 | def __init__(self, index_file_path, html_output_path, templates_path): 8 | self.contents_reader = ContentsReader(index_file_path) 9 | self.html_output_path = html_output_path 10 | self.templates_path = templates_path 11 | # TODO: this could be customizeable 12 | self.index_url = "story_index.html" 13 | 14 | def write_zine_html(self): 15 | intro_text = self.contents_reader.read_hello_file(wrap_returns=False) 16 | zine_dict = self.contents_reader.read_story_object(include_contents=True) 17 | 18 | intro_template_path = f'{self.templates_path}/intro.html' 19 | index_template_path = f'{self.templates_path}/story_index.html' 20 | story_template_path = f'{self.templates_path}/story_page.html' 21 | 22 | Path(self.html_output_path).mkdir(parents=True, exist_ok=True) 23 | 24 | self._write_intro_html(intro_template_path, intro_text) 25 | self._write_index_html(index_template_path, zine_dict) 26 | self._write_stories_html(story_template_path, zine_dict) 27 | 28 | def _write_intro_html(self, intro_template_path, intro_text): 29 | intro_html = '' 30 | html_text = [self._htmlize_whitespace(x) for x in intro_text] 31 | with open(intro_template_path, 'r') as f: 32 | intro_html = chevron.render(f, {'intro_lines': html_text, 'index_url': self.index_url}) 33 | file_output_path = self.html_output_path + "/index.html" 34 | print("Writing intro page to: %s" % file_output_path) 35 | with open(file_output_path, 'w') as f: 36 | f.write(intro_html) 37 | 38 | def _write_index_html(self, index_template_path, zine_dict): 39 | index_html = '' 40 | with open(index_template_path, 'r') as f: 41 | index_items = [] 42 | for index in range(len(zine_dict)): 43 | item = zine_dict[index] 44 | first_page_link = "%s/1.html" % item['directory'] 45 | item_dict = { 46 | 'title_text': index_item_string(index, item['title'], item['author'], include_linebreaks=False), 47 | 'story_link': first_page_link 48 | } 49 | index_items.append(item_dict) 50 | index_html = chevron.render(f, { 51 | 'header': index_header(include_linebreaks=False), 52 | 'stories': index_items, 53 | 'index_url': self.index_url 54 | }) 55 | file_output_path = self.html_output_path + "/" + self.index_url 56 | print("Writing index page to: %s" % file_output_path) 57 | with open(file_output_path, 'w') as f: 58 | f.write(index_html) 59 | 60 | def _write_stories_html(self, story_page_template_path, zine_dict): 61 | story_template = '' 62 | with open(story_page_template_path, 'r') as f: 63 | story_template = f.read() 64 | 65 | for item in zine_dict: 66 | output_directory = item['directory'] 67 | story_contents = item['contents'] 68 | for index, page in enumerate(story_contents): 69 | # pages are 1-indexed, just to make them a bit more human readable 70 | current_page = index + 1 71 | previous_page = f'{str(current_page - 1)}.html' if index > 0 else '' 72 | next_page = f'{str(current_page + 1)}.html' if index < len(story_contents) -1 else '' 73 | page_html = chevron.render(story_template, { 74 | 'back_link': previous_page, 75 | 'next_link': next_page, 76 | 'story_lines': [self._htmlize_whitespace(x.replace("\n", "")) for x in page], 77 | 'index_url': '../story_index.html', 78 | 'story_title': item['title'], 79 | 'story_author': item['author'], 80 | 'page_number': current_page, 81 | }) 82 | file_output_dir = self.html_output_path + "/" + output_directory + "/" 83 | file_output_path = file_output_dir + str(current_page) + ".html" 84 | Path(file_output_dir).mkdir(parents=True, exist_ok=True) 85 | print("Writing story page to: %s" % file_output_path) 86 | with open(file_output_path, 'w') as f: 87 | f.write(page_html) 88 | 89 | def _htmlize_whitespace(self, string): 90 | # linebreaks are already determined via newlines in source text files. 91 | # we can (and should) just make all spaces non-breaking. 92 | # TODO: evaluate this with screen readers 93 | return string.replace(' ', ' ') 94 | --------------------------------------------------------------------------------