commit 5c2d5626d0a087d5e8d377b8b00189d1547891ac Author: Tyrel Souza Date: Sat Jul 29 23:26:31 2023 -0400 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..802e12b --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +.vscode/ +tags +tags.* +venv/ +__pycache__/ +issue* +dial_a_zine_issue* +newsession_issue2 diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..9afe07b --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "issue1"] + path = issue1 + url = git@github.com:caraesten/dial_a_zine_issue1.git diff --git a/1x 40 right.txt b/1x 40 right.txt new file mode 100644 index 0000000..a31e771 --- /dev/null +++ b/1x 40 right.txt @@ -0,0 +1,24 @@ + o--------------------------------------o + | | + | | + | | + | | + | | + | | + | | + | | + | | + this page intentionally left blank | | + | | + | | + | | + | | + | | + | | + | | + | | + | | + | | + | | + | | + o--------------------------------------o \ No newline at end of file diff --git a/1x 40.txt b/1x 40.txt new file mode 100644 index 0000000..1a439a4 --- /dev/null +++ b/1x 40.txt @@ -0,0 +1,24 @@ +o--------------------------------------o +| | +| | +| | +| | +| | +| | +| | +| | +| | +| | +| | +| | +| | +| | +| | +| | +| | +| | +| | +| | +| | +| | +o--------------------------------------o \ No newline at end of file diff --git a/1x 80.txt b/1x 80.txt new file mode 100644 index 0000000..5652fae --- /dev/null +++ b/1x 80.txt @@ -0,0 +1,24 @@ +o-----------------------------------------------------------------------------o +| | +| | +| | +| | +| | +| | +| | +| | +| | +| | +| | +| | +| | +| | +| | +| | +| | +| | +| | +| | +| | +| | +o-----------------------------------------------------------------------------o \ No newline at end of file diff --git a/2x 40.txt b/2x 40.txt new file mode 100644 index 0000000..43d0c02 --- /dev/null +++ b/2x 40.txt @@ -0,0 +1,24 @@ +o--------------------------------------o--------------------------------------o +| | | +| | | +| | | +| | | +| | | +| | | +| | | +| | | +| | | +| | | +| | | +| | | +| | | +| | | +| | | +| | | +| | | +| | | +| | | +| | | +| | | +| | | +o--------------------------------------o--------------------------------------o \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..56227e7 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,17 @@ +# syntax=docker/dockerfile:1 + +ARG ISSUE_DIR=issue1 + +FROM python:3.8-slim-buster + +WORKDIR /app + +COPY requirements.txt requirements.txt +RUN pip3 install -r requirements.txt + +COPY dialazine dialazine +COPY $ISSUE_DIR $ISSUE_DIR + +EXPOSE 23/tcp + +CMD [ "python3", "dialazine/server.py" ] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f56b1f1 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Cara Esten Hurtle + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..1947019 --- /dev/null +++ b/README.md @@ -0,0 +1,19 @@ +## How to run + +To run locally, run server.py as a python file e.g. from the root directory + +`python3 dialazine/server.py` + +To access locally, run + +`telnet localhost 23` + +(or equivalent with another telnet client) + +in server.py, change CONTENT_FOLDER to "example_issue" to see example + +## Zine Structure + +In the index.json, the "hello" message is the filepath within the issue folder for what is first displayed, + +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 diff --git a/default_templates/intro.html b/default_templates/intro.html new file mode 100644 index 0000000..1ffd419 --- /dev/null +++ b/default_templates/intro.html @@ -0,0 +1,18 @@ + + + + + + +
+
+ {{#intro_lines}} + {{{.}}}
+ {{/intro_lines}} +
+ +
+ + diff --git a/default_templates/main_sample.css b/default_templates/main_sample.css new file mode 100644 index 0000000..8a1ecf4 --- /dev/null +++ b/default_templates/main_sample.css @@ -0,0 +1,44 @@ +body { + font-family: monospace; + background-color: black; + color: #39ff14; +} + +a { + color: #ddd; +} + +h2 { + text-align: center; +} + +.main { + width: 82ch; /* fudge it a bit */ + margin-left: auto; + margin-right: auto; +} + +.viewport { + height: 30em; + margin-top: 2em; +} + +.stories-list li { + margin: 0.5em; +} + +.navigation { + margin-top: 2em; + text-align: center; +} + +.navigation a { + display: inline-block; +} + +.about { + color: #1c830a; + text-align: center; + font-size: 10pt; + margin-top: 1em; +} diff --git a/default_templates/story_index.html b/default_templates/story_index.html new file mode 100644 index 0000000..faf7dc1 --- /dev/null +++ b/default_templates/story_index.html @@ -0,0 +1,24 @@ + + + + + + +
+
+

{{header}}

+ +
+ +
+ Sample zine, generated by dial-a-zine telnet cms! +
+
+ + diff --git a/default_templates/story_page.html b/default_templates/story_page.html new file mode 100644 index 0000000..622382f --- /dev/null +++ b/default_templates/story_page.html @@ -0,0 +1,33 @@ + + + + + + +
+
+ {{#story_lines}} + {{{.}}}
+ {{/story_lines}} +
+ +
+ {{story_title}} by {{story_author}}, page {{page_number}} +
+
+ + diff --git a/dialazine/generate_html.py b/dialazine/generate_html.py new file mode 100755 index 0000000..5d52d65 --- /dev/null +++ b/dialazine/generate_html.py @@ -0,0 +1,24 @@ +#!/usr/bin/python3 +import os +import argparse +from pathlib import Path +from lib.html_generator import HtmlGenerator + +TEMPLATES = "default_templates" +CONTENT_FOLDER = "example_issue" + +parser = argparse.ArgumentParser(description="Use this tool to export html documents for your zine") +parser.add_argument("outputdir", type=Path, help="output directory for generated HTML") + +args = parser.parse_args() + +if not args.outputdir: + raise ValueError("Required param outputdir missing, run program with -h") + +html_directory = args.outputdir.as_posix() + +root_dir_path = Path(__file__).parent.parent.absolute() +index_directory = "%s/%s" % (root_dir_path.as_posix(), f"{CONTENT_FOLDER}/index.json") +templates_directory = "%s/%s" % (root_dir_path.as_posix(), TEMPLATES) +generator = HtmlGenerator(index_directory, html_directory, templates_directory) +generator.write_zine_html() \ No newline at end of file diff --git a/dialazine/lib/common_tools.py b/dialazine/lib/common_tools.py new file mode 100644 index 0000000..5f47150 --- /dev/null +++ b/dialazine/lib/common_tools.py @@ -0,0 +1,12 @@ + +def index_header(include_linebreaks=True): + linebreak_char = '' + if include_linebreaks: + linebreak_char = '\n' + return linebreak_char + " [ INDEX ] " + linebreak_char + +def index_item_string(option, title, author, include_linebreaks=True): + linebreak_char = '' + if include_linebreaks: + linebreak_char = '\n' + return linebreak_char + ("%s > %s < ...by %s" % (option, title, author)) + linebreak_char diff --git a/dialazine/lib/contents_reader.py b/dialazine/lib/contents_reader.py new file mode 100644 index 0000000..e98bd75 --- /dev/null +++ b/dialazine/lib/contents_reader.py @@ -0,0 +1,92 @@ +import json, os, string +from lib.common_tools import index_item_string +from lib.text_screen_reader import TextScreenReader +from lib.common_tools import index_header + +class ContentsReader: + def __init__(self, contents_file_path): + self.issue_directory = os.path.dirname(contents_file_path) + self.text_reader = TextScreenReader(self.issue_directory) + contents_file = open(contents_file_path, 'r') + self.contents_json = json.load(contents_file) + self.verify_contents() + + def verify_contents(self): + if not self.contents_json['hello']: + raise Exception("Contents JSON requires a hello message") + + def hello_file_path(self): + return self.contents_json['hello'] + + def read_hello_file(self, wrap_returns=True): + lines = self.text_reader.read_file_name(self.hello_file_path()) + if wrap_returns: + return self._wrap_carriage_returns(lines) + else: + return lines + + def read_index_lines(self): + index_lines = [index_header()] + option_number = 1 + for index_item in self.read_story_object(): + index_lines.append(index_item_string(self._index_to_option(option_number), index_item['title'], index_item['author'])) + option_number += 1 + index_lines.append("\n(or X to quit!)\n") + return self._wrap_carriage_returns(index_lines) + + def read_story_object(self, include_contents=False): + index_list = [] + story_number = 1 + for index_item in self.contents_json['contents']: + story_item = { + 'title': index_item['title'], + 'author': index_item['author'], + 'directory': index_item['directory'], + } + if include_contents: + page_number = 1 + story_lines = self.read_story(story_number, page_number, wrap_returns=False) + pages = [] + while len(story_lines) > 0: + pages.append(story_lines) + page_number += 1 + story_lines = self.read_story(story_number, page_number, wrap_returns=False) + story_item['contents'] = pages + story_number += 1 + index_list.append(story_item) + return index_list + + def read_story(self, story_number, page = 1, wrap_returns=True): + if story_number > len(self.contents_json['contents']): + return [] + story_obj = self.contents_json['contents'][story_number - 1] + file_path = "%s/%s.txt" % (story_obj['directory'], page) + + if self.text_reader.does_file_exist(file_path): + page_lines = self.text_reader.read_file_name(file_path) + if wrap_returns: + return self._wrap_carriage_returns(page_lines) + else: + return page_lines + else: + return [] + + def _wrap_carriage_returns(self, lines_list): + return [x + '\r' for x in lines_list] + + def map_input_to_numerical_index(self, input_string): + try: + return int(input_string) + except ValueError: + pass + try: + return 10 + string.ascii_uppercase[0:10].index(input_string) + except ValueError: + return -1 + + def _index_to_option(self, input_index): + if (input_index < 10): + return input_index + else: + return string.ascii_uppercase[input_index - 10] + diff --git a/dialazine/lib/html_generator.py b/dialazine/lib/html_generator.py new file mode 100644 index 0000000..3d88867 --- /dev/null +++ b/dialazine/lib/html_generator.py @@ -0,0 +1,93 @@ +import chevron +from pathlib import Path +from lib.common_tools import index_item_string, index_header +from lib.contents_reader import ContentsReader + +class HtmlGenerator: + def __init__(self, index_file_path, html_output_path, templates_path): + self.contents_reader = ContentsReader(index_file_path) + self.html_output_path = html_output_path + self.templates_path = templates_path + # TODO: this could be customizeable + self.index_url = "story_index.html" + + def write_zine_html(self): + intro_text = self.contents_reader.read_hello_file(wrap_returns=False) + zine_dict = self.contents_reader.read_story_object(include_contents=True) + + intro_template_path = f'{self.templates_path}/intro.html' + index_template_path = f'{self.templates_path}/story_index.html' + story_template_path = f'{self.templates_path}/story_page.html' + + Path(self.html_output_path).mkdir(parents=True, exist_ok=True) + + self._write_intro_html(intro_template_path, intro_text) + self._write_index_html(index_template_path, zine_dict) + self._write_stories_html(story_template_path, zine_dict) + + def _write_intro_html(self, intro_template_path, intro_text): + intro_html = '' + html_text = [self._htmlize_whitespace(x) for x in intro_text] + with open(intro_template_path, 'r') as f: + intro_html = chevron.render(f, {'intro_lines': html_text, 'index_url': self.index_url}) + file_output_path = self.html_output_path + "/index.html" + print("Writing intro page to: %s" % file_output_path) + with open(file_output_path, 'w') as f: + f.write(intro_html) + + def _write_index_html(self, index_template_path, zine_dict): + index_html = '' + with open(index_template_path, 'r') as f: + index_items = [] + for index in range(len(zine_dict)): + item = zine_dict[index] + first_page_link = "%s/1.html" % item['directory'] + item_dict = { + 'title_text': index_item_string(index, item['title'], item['author'], include_linebreaks=False), + 'story_link': first_page_link + } + index_items.append(item_dict) + index_html = chevron.render(f, { + 'header': index_header(include_linebreaks=False), + 'stories': index_items, + 'index_url': self.index_url + }) + file_output_path = self.html_output_path + "/" + self.index_url + print("Writing index page to: %s" % file_output_path) + with open(file_output_path, 'w') as f: + f.write(index_html) + + def _write_stories_html(self, story_page_template_path, zine_dict): + story_template = '' + with open(story_page_template_path, 'r') as f: + story_template = f.read() + + for item in zine_dict: + output_directory = item['directory'] + story_contents = item['contents'] + for index, page in enumerate(story_contents): + # pages are 1-indexed, just to make them a bit more human readable + current_page = index + 1 + previous_page = f'{str(current_page - 1)}.html' if index > 0 else '' + next_page = f'{str(current_page + 1)}.html' if index < len(story_contents) -1 else '' + page_html = chevron.render(story_template, { + 'back_link': previous_page, + 'next_link': next_page, + 'story_lines': [self._htmlize_whitespace(x.replace("\n", "")) for x in page], + 'index_url': '../story_index.html', + 'story_title': item['title'], + 'story_author': item['author'], + 'page_number': current_page, + }) + file_output_dir = self.html_output_path + "/" + output_directory + "/" + file_output_path = file_output_dir + str(current_page) + ".html" + Path(file_output_dir).mkdir(parents=True, exist_ok=True) + print("Writing story page to: %s" % file_output_path) + with open(file_output_path, 'w') as f: + f.write(page_html) + + def _htmlize_whitespace(self, string): + # linebreaks are already determined via newlines in source text files. + # we can (and should) just make all spaces non-breaking. + # TODO: evaluate this with screen readers + return string.replace(' ', ' ') diff --git a/dialazine/lib/text_screen_reader.py b/dialazine/lib/text_screen_reader.py new file mode 100644 index 0000000..edea61b --- /dev/null +++ b/dialazine/lib/text_screen_reader.py @@ -0,0 +1,13 @@ +import os + +class TextScreenReader: + def __init__(self, root_directory): + self.root_directory = root_directory + def read_file_name(self, path): + full_path = "%s/%s" % (self.root_directory, path) + with open(full_path, 'r') as f: + full_file = f.readlines() + f.close() + return full_file + def does_file_exist(self, file_path): + return os.path.exists("%s/%s" % (self.root_directory, file_path)) diff --git a/dialazine/lib/zine_functions.py b/dialazine/lib/zine_functions.py new file mode 100644 index 0000000..b29dbca --- /dev/null +++ b/dialazine/lib/zine_functions.py @@ -0,0 +1,54 @@ +from lib.contents_reader import ContentsReader +import asyncio + +CLEAR_SCREEN = "\u001b[2J" +NEW_LINE = "\r\n" + +class ZineFunctions: + def __init__(self, reader, writer, index_file_path): + self.reader = reader + self.writer = writer + self.contents_reader = ContentsReader(index_file_path) + + async def run_index(self): + for welcome_line in self.contents_reader.read_hello_file(): + self.writer.write(welcome_line) + await self.writer.drain() + # Read one byte (any key) + await self.reader.read(1) + running = True + while (running): + for index_line in self.contents_reader.read_index_lines(): + self.writer.write(index_line) + item_choice = await self.reader.read(1) + item_choice_int = -1 + if item_choice.upper() == 'X': + running = False + continue + item_choice_int = self.contents_reader.map_input_to_numerical_index(item_choice) + if item_choice_int == -1: + self.writer.write(f"{NEW_LINE}{NEW_LINE}Pick a story, or X to quit.{NEW_LINE}") + continue + self.writer.write(f"{NEW_LINE}{NEW_LINE}...you picked: %s" % (item_choice)) + self.writer.write(f"{NEW_LINE}{NEW_LINE}...press RETURN to start reading, and to continue after each page") + await self.reader.read(1) + self.writer.write(NEW_LINE + CLEAR_SCREEN) + await asyncio.sleep(1) + await self.run_story(item_choice_int) + self.disconnect() + + async def run_story(self, story_number): + page_number = 1 + + story_lines = self.contents_reader.read_story(story_number, page_number) + while len(story_lines) > 0: + self.writer.write(CLEAR_SCREEN) + for story_line in story_lines: + self.writer.write(story_line) + await self.writer.drain() + char_read = await self.reader.readline() + page_number += 1 + story_lines = self.contents_reader.read_story(story_number, page_number) + + def disconnect(self): + self.writer.close() diff --git a/dialazine/server.py b/dialazine/server.py new file mode 100755 index 0000000..e9e006d --- /dev/null +++ b/dialazine/server.py @@ -0,0 +1,19 @@ +#!/usr/bin/python3 +import asyncio, telnetlib3 +import os +import pathlib +from lib.zine_functions import ZineFunctions + +LOCALHOST_PORT = 23 +CONTENT_FOLDER = "tyrel" + +async def shell(reader, writer): + root_dir_path = pathlib.Path(__file__).parent.parent.absolute() + + zine = ZineFunctions(reader, writer, "%s/%s" % (root_dir_path.as_posix(), f"{CONTENT_FOLDER}/index.json")) + await zine.run_index() + +loop = asyncio.get_event_loop() +srv = telnetlib3.create_server(port=LOCALHOST_PORT, shell=shell, timeout=3600) +server = loop.run_until_complete(srv) +loop.run_until_complete(server.wait_closed()) diff --git a/dialazine/tools/proof_ascii.html b/dialazine/tools/proof_ascii.html new file mode 100644 index 0000000..c78c42f --- /dev/null +++ b/dialazine/tools/proof_ascii.html @@ -0,0 +1,53 @@ + + + + ASCII Scanner + + + + +
+

ASCII Scanner

+

Paste Text Below

+ +
+
+
+ + \ No newline at end of file diff --git a/dialazine/tools/proof_ascii.js b/dialazine/tools/proof_ascii.js new file mode 100644 index 0000000..8681991 --- /dev/null +++ b/dialazine/tools/proof_ascii.js @@ -0,0 +1,34 @@ +class AsciiScanner { + static ASCII_REGEX = /([\u{00e1}-\u{FFFF}]+)/gu; + constructor(inputElement, matchesOutput, textOutput) { + this.inputElement = inputElement; + this.matchesOutput = matchesOutput; + this.textOutput = textOutput; + } + + start() { + this.inputElement.addEventListener('input', (event) => { + const text = event.target.value; + this.onTextChange(text); + }) + } + + onTextChange(newText) { + const output = newText.replace(AsciiScanner.ASCII_REGEX, (match) => { + return `${match}`; + }); + this.textOutput.innerHTML = output; + + const errors = ((newText || '').match(AsciiScanner.ASCII_REGEX) || []).length; + this.matchesOutput.innerHTML = `Found: ${errors} non-ASCII blocks` + } +} + +window.onload = function() { + const scanner = new AsciiScanner( + document.getElementById('text-input'), + document.getElementById('matches-output'), + document.getElementById('text-output')); + + scanner.start(); +} diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..1006d18 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +telnetlib3==1.0.4 +chevron==0.14.0 diff --git a/tyrel/index.json b/tyrel/index.json new file mode 100644 index 0000000..2a7c9bb --- /dev/null +++ b/tyrel/index.json @@ -0,0 +1,10 @@ +{ + "hello": "intro.txt", + "contents": [ + { + "title": "Issue 1 - Intro", + "author": "Tyrel Souza", + "directory": "iss001" + } + ] +} \ No newline at end of file diff --git a/tyrel/intro.txt b/tyrel/intro.txt new file mode 100644 index 0000000..a7275fe --- /dev/null +++ b/tyrel/intro.txt @@ -0,0 +1,23 @@ + + + + + + + + + + + + + Welcome to Tyrel's Zine. + Will keep updated at a very infrequent rate. + + + - Tyrel Souza + + + + + + diff --git a/tyrel/iss001/1.txt b/tyrel/iss001/1.txt new file mode 100644 index 0000000..8dbc208 --- /dev/null +++ b/tyrel/iss001/1.txt @@ -0,0 +1,24 @@ + o--------------------------------------o + | | + | | + | | + | ___ ___ ___ | + | /\ \ /\ \ /\ \ | + | \:\ \ /::\ \ /::\ \ | + | /::\__\ /::\:\__\ /\:\:\__\ | + | /:/\/__/ \/\::/ / \:\:\/__/ | + | \/__/ /:/ / \::/ / | + | \/__/ \/__/ | + this page intentionally left blank | | + | tyrel | + | anthony | + | souza | + | | + | | + | | + | | + | | + | | + | | + | 1 | +2023-07-29 o--------------------------------------o \ No newline at end of file diff --git a/tyrel/iss001/2.txt b/tyrel/iss001/2.txt new file mode 100644 index 0000000..8ba9fa0 --- /dev/null +++ b/tyrel/iss001/2.txt @@ -0,0 +1,24 @@ +o--------------------------------------o--------------------------------------o +| \ | +| Welcome to my zine / Wow so apparently I have a telnet | +| ------------------ \ zine now. | +| / | +| Everything in all its ASCII \ Not sure what to put here yet! | +| glory. / | +| \ You can find me --\ | +| / | | +| \ | | +| / | | +| \ https://tyrel.dev/links