This commit is contained in:
Tyrel Souza 2023-07-29 23:26:31 -04:00
commit 5c2d5626d0
No known key found for this signature in database
GPG Key ID: F3614B02ACBE438E
27 changed files with 760 additions and 0 deletions

8
.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
.vscode/
tags
tags.*
venv/
__pycache__/
issue*
dial_a_zine_issue*
newsession_issue2

3
.gitmodules vendored Normal file
View File

@ -0,0 +1,3 @@
[submodule "issue1"]
path = issue1
url = git@github.com:caraesten/dial_a_zine_issue1.git

24
1x 40 right.txt Normal file
View File

@ -0,0 +1,24 @@
o--------------------------------------o
| |
| |
| |
| |
| |
| |
| |
| |
| |
this page intentionally left blank | |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
o--------------------------------------o

24
1x 40.txt Normal file
View File

@ -0,0 +1,24 @@
o--------------------------------------o
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
o--------------------------------------o

24
1x 80.txt Normal file
View File

@ -0,0 +1,24 @@
o-----------------------------------------------------------------------------o
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
o-----------------------------------------------------------------------------o

24
2x 40.txt Normal file
View File

@ -0,0 +1,24 @@
o--------------------------------------o--------------------------------------o
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
o--------------------------------------o--------------------------------------o

17
Dockerfile Normal file
View File

@ -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" ]

21
LICENSE Normal file
View File

@ -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.

19
README.md Normal file
View File

@ -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

View File

@ -0,0 +1,18 @@
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="main.css">
</head>
<body>
<div class="main">
<div class="viewport">
{{#intro_lines}}
{{{.}}} <br>
{{/intro_lines}}
</div>
<div class="navigation">
<a href="{{ index_url }}">(index)</a>
</div>
</div>
</body>
</html>

View File

@ -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;
}

View File

@ -0,0 +1,24 @@
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="main.css">
</head>
<body>
<div class="main">
<div class="viewport">
<h2>{{header}}</h2>
<ul class="stories-list">
{{#stories}}
<li><a href='{{story_link}}'>{{title_text}}</a></li>
{{/stories}}
</ul>
</div>
<div class="navigation">
<a href="index.html">(intro)</a>
</div>
<div class="about">
Sample zine, generated by <a href="https://github.com/caraesten/dial_a_zine">dial-a-zine</a> telnet cms!
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,33 @@
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="../main.css">
</head>
<body>
<div class="main">
<div class="viewport">
{{#story_lines}}
{{{.}}} <br>
{{/story_lines}}
</div>
<div class="navigation">
{{#back_link}}
<a href="{{.}}">back</a>
{{/back_link}}
{{^back_link}}
back
{{/back_link}}
<a href="{{ index_url }}">(index)</a>
{{#next_link}}
<a href="{{.}}">next</a>
{{/next_link}}
{{^next_link}}
next
{{/next_link}}
</div>
<div class="about">
{{story_title}} by {{story_author}}, page {{page_number}}
</div>
</div>
</body>
</html>

24
dialazine/generate_html.py Executable file
View File

@ -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()

View File

@ -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

View File

@ -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]

View File

@ -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(' ', '&nbsp;')

View File

@ -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))

View File

@ -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()

19
dialazine/server.py Executable file
View File

@ -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())

View File

@ -0,0 +1,53 @@
<!DOCTYPE html>
<html>
<head>
<title>ASCII Scanner</title>
<script type="text/javascript" src="proof_ascii.js"></script>
<style type="text/css">
body {
background-color: #444;
color: #ddd;
}
#main-body {
width: 60%;
margin-left: auto;
margin-right: auto;
text-align: center;
}
textarea {
background-color: #666;
}
#text-input {
width: 90%;
margin-left: auto;
margin-right: auto;
min-height: 20em;
color: #ddd;
}
#matches-output {
margin-top: 1em;
background-color: #666;
width: 90%;
margin-left: auto;
margin-right: auto;
}
#text-output {
margin-top: 1em;
text-align: left;
}
#text-output .invalid {
background-color: #d66;
}
</style>
</head>
<body>
<div id="main-body">
<h1>ASCII Scanner</h1>
<h3>Paste Text Below</h3>
<textarea id="text-input"></textarea>
<div id="matches-output"></div>
<div id="text-output"></div>
</div>
</body>
</html>

View File

@ -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 `<span class='invalid'>${match}</span>`;
});
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();
}

2
requirements.txt Normal file
View File

@ -0,0 +1,2 @@
telnetlib3==1.0.4
chevron==0.14.0

10
tyrel/index.json Normal file
View File

@ -0,0 +1,10 @@
{
"hello": "intro.txt",
"contents": [
{
"title": "Issue 1 - Intro",
"author": "Tyrel Souza",
"directory": "iss001"
}
]
}

23
tyrel/intro.txt Normal file
View File

@ -0,0 +1,23 @@
Welcome to Tyrel's Zine.
Will keep updated at a very infrequent rate.
- Tyrel Souza

24
tyrel/iss001/1.txt Normal file
View File

@ -0,0 +1,24 @@
o--------------------------------------o
| |
| |
| |
| ___ ___ ___ |
| /\ \ /\ \ /\ \ |
| \:\ \ /::\ \ /::\ \ |
| /::\__\ /::\:\__\ /\:\:\__\ |
| /:/\/__/ \/\::/ / \:\:\/__/ |
| \/__/ /:/ / \::/ / |
| \/__/ \/__/ |
this page intentionally left blank | |
| tyrel |
| anthony |
| souza |
| |
| |
| |
| |
| |
| |
| |
| 1 |
2023-07-29 o--------------------------------------o

24
tyrel/iss001/2.txt Normal file
View File

@ -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 </ |
| / @tyrel:tyrel.dev |
| \ email@tyrel.dev |
| / @tyrel@tyrel.social |
| \ |
| / |
| \ |
| / |
| \ |
| / |
| \ |
| 2 / 3 |
o--------------------------------------o--------------------------------------o