├── 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 |
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 |
8 |
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 |
8 |
9 | {{#story_lines}}
10 | {{{.}}}
11 | {{/story_lines}}
12 |
13 |
14 | {{#back_link}}
15 |
back
16 | {{/back_link}}
17 | {{^back_link}}
18 | back
19 | {{/back_link}}
20 |
(index)
21 | {{#next_link}}
22 |
next
23 | {{/next_link}}
24 | {{^next_link}}
25 | next
26 | {{/next_link}}
27 |
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 `