Compare commits

...

10 commits

Author SHA1 Message Date
my 01ae1d7e92 handle yaml errors 2026-01-29 12:24:19 +01:00
my c661d09926 update ignore 2026-01-29 12:22:31 +01:00
my 44d5da95fe add cover to imposed view 2026-01-28 21:52:23 +01:00
my baf51cb651 fix footnotes 2026-01-22 11:48:31 +01:00
my 27d59283a3 update pagedjs to latests 2026-01-22 11:33:19 +01:00
Heerko ef640752cc import changes from lepa 2025-12-27 14:51:12 +01:00
Heerko 378f37c9b9 exclude venv from git 2025-12-24 18:27:46 +01:00
cc 27ad48d389 small changes 2025-11-30 09:12:01 +00:00
cc 98608ca168 changed the default text for new octomode environments 2025-11-30 09:12:01 +00:00
mb 6923da1dc0 Update README.md 2025-01-09 15:28:08 +01:00
19 changed files with 6233 additions and 3306 deletions

2
.gitignore vendored
View file

@ -1 +1,3 @@
.env
.venv/
__pycache__

View file

@ -91,6 +91,8 @@ This will first check if you already install gunicorn in the `.venv` folder, and
## Which browser to use?
> **WARNING**: these warnings sound so official.
> **WARNING**: When working collectively, it's recommended to use the same browser
> and if possible also the same version of that browser, to get the same (or at least
> the most similar) PDF. Different operating systems and browsers render CSS rules

18
filter_registry.py Normal file
View file

@ -0,0 +1,18 @@
# This is an attempt to create a customizable way to
# add filters to the content coming from the etherpad
# get_text api endpoint
# in pad_filters.py do this:
#
# import re
# from filter_registry import register_filter
# @register_filter
# def remove_star_before_img(text):
# return re.sub(r'\*\s*!?\[\]\(', '![](', text)
pad_content_filters = []
def register_filter(func):
print(f"Registering filter: {func.__name__}")
pad_content_filters.append(func)
return func

View file

@ -3,6 +3,7 @@ import json
from flask import Flask, request, render_template, redirect, url_for
from urllib.request import urlopen
from urllib.parse import urlencode
import html
# To sanitize Flask input fields
from markupsafe import Markup, escape
@ -14,11 +15,23 @@ import pypandoc
# To read the Markdown metadat
import markdown
from filter_registry import pad_content_filters
import pad_filters
APP = Flask(__name__)
APP.config.from_pyfile('settings.py')
# ---
# my attempt as showing helpful error messages.
class MarkdownRenderError(RuntimeError):
def __init__(self, kind, message, original):
super().__init__(message)
self.kind = kind
self.original = original
def get_pad_content(pad_name, ext=""):
if ext:
pad_name = f'{ pad_name }{ ext }'
@ -40,6 +53,11 @@ def get_pad_content(pad_name, ext=""):
response = json.load(urlopen(f"{ APP.config['PAD_API_URL'] }/{ api_call }", data=urlencode(arguments).encode()))
content = response['data']['text']
# print("before: " + content)
print( "GET PAD CONTENT" )
for f in pad_content_filters:
content = f(content)
# print("after: " + content)
return content
def all_pads():
@ -51,6 +69,22 @@ def all_pads():
return response
# get all pads that end in .md
def all_publications():
pads = all_pads()
pubs = []
print(pads)
if(pads and pads['data'] and pads['data']['padIDs']):
for pad in pads['data']['padIDs']:
# if extension is .md add it to pubs
if pad.endswith('.md'):
# strip the .md
name = pad.removesuffix('.md')
pubs.append(name)
return pubs
else:
return []
def create_pad_on_first_run(name, ext):
pads = all_pads()
pad = name+ext
@ -76,7 +110,21 @@ def create_pad_on_first_run(name, ext):
def md_to_html(md_pad_content):
# Convert Markdown to HTML
# html = markdown.markdown(md_pad_content, extensions=['meta', 'attr_list']) # attr_list does not work
html = pypandoc.convert_text(md_pad_content, 'html', format='md')
try:
html = pypandoc.convert_text(md_pad_content, 'html', format='md')
except RuntimeError as exc:
message = str(exc)
if "YAML" in message or "YAML metadata" in message:
raise MarkdownRenderError(
kind="yaml",
message="Invalid YAML metadata. Use `key: value` and add a blank line.",
original=str(exc)
) from exc
raise MarkdownRenderError(
kind="markdown",
message = "Markdown conversion failed. Please check the markdown for errors.",
original = str(exc)
) from exc
# Sanitize the Markdown
# html = bleach.clean(html)
@ -95,6 +143,36 @@ def get_md_metadata(md_pad_content):
return metadata
def get_app_root():
if APP.config['APPLICATION_ROOT'] == '/':
app_root = ''
elif APP.config['APPLICATION_ROOT'].endswith('/'):
app_root = APP.config['APPLICATION_ROOT'][:-1]
else:
app_root = APP.config['APPLICATION_ROOT']
return app_root
# def apply_cover(html_str, cover):
# import html
# html_str = str(html_str)
# html_str = html_str.replace('class="cover"', "class='cover' style='background-image: url("+ cover +")'")
# return Markup(html_str)
def get_meta(metadata, key, default=None):
return metadata.get(key, [default])[0]
def render_markdown_error(name, error):
print("ERROR", error)
return render_template(
'error.html',
name=name.strip(),
error_text=str(error),
error=error,
lang="en",
title="Markdown error"
), 400
# ---
@APP.route('/', methods=['GET', 'POST'])
@ -110,7 +188,8 @@ def index():
create_pad_on_first_run(name, ext)
return redirect(url_for("pad", name=name))
else:
return render_template('start.html', pad_url=APP.config['PAD_URL'])
pubs = all_publications()
return render_template('start.html', pubs = pubs, home_pad_url=APP.config['HOME_PAD_URL'])
@APP.route('/<name>/')
def main(name):
@ -128,28 +207,22 @@ def stylesheet(name):
@APP.route('/<name>/html/')
def html(name):
# only here we need application root to make all the URLs work.....
if APP.config['APPLICATION_ROOT'] == '/':
app_root = ''
elif APP.config['APPLICATION_ROOT'].endswith('/'):
app_root = APP.config['APPLICATION_ROOT'][:-1]
else:
app_root = APP.config['APPLICATION_ROOT']
app_root = get_app_root()
url = f"{ app_root }/{ name }/preview.html"
return render_template('iframe.html', url=url, name=name.strip(), pad_url=APP.config['PAD_URL'])
@APP.route('/<name>/pdf/')
def pdf(name):
# only here we need application root to make all the URLs work.....
if APP.config['APPLICATION_ROOT'] == '/':
app_root = ''
elif APP.config['APPLICATION_ROOT'].endswith('/'):
app_root = APP.config['APPLICATION_ROOT'][:-1]
else:
app_root = APP.config['APPLICATION_ROOT']
app_root = get_app_root()
url = f"{ app_root }/{name}/pagedjs.html"
return render_template('pdf.html', url=url, name=name.strip(), pad_url=APP.config['PAD_URL'])
@APP.route('/<name>/impose/')
def impose(name):
app_root = get_app_root()
url = f"{ app_root }/{name}/imposed.html"
return render_template('pdf.html', url=url, name=name.strip(), pad_url=APP.config['PAD_URL'])
# //////////////////
# RENDERED RESOURCES
# //////////////////
@ -166,14 +239,13 @@ def css(name):
def preview(name):
# TO GENERATE THE PREVIEW WEBPAGE
md_pad_content = get_pad_content(name, ext='.md')
html = md_to_html(md_pad_content)
try:
html = md_to_html(md_pad_content)
except MarkdownRenderError as exc:
return render_markdown_error(name, exc)
metadata = get_md_metadata(md_pad_content)
if metadata:
lang = metadata['language'][0]
title = metadata['title'][0]
else:
lang = "en"
title = "No title"
lang = get_meta(metadata, 'language', 'en')
title = get_meta(metadata, 'title', 'Untitled')
return render_template('preview.html', name=name.strip(), pad_content=html, lang=lang, title=title)
@ -181,16 +253,40 @@ def preview(name):
def pagedjs(name):
# TO GENERATE THE PAGED.JS WEBPAGE
md_pad_content = get_pad_content(name, ext='.md')
html = md_to_html(md_pad_content)
try:
html = md_to_html(md_pad_content)
except MarkdownRenderError as exc:
return render_markdown_error(name, exc)
metadata = get_md_metadata(md_pad_content)
lang = metadata['language'][0]
title = metadata['title'][0]
lang = get_meta(metadata, 'language', 'en')
title = get_meta(metadata, 'title', 'Untitled')
cover = get_meta(metadata, 'cover', None)
return render_template('pagedjs.html', name=name.strip(), pad_content=html, lang=lang, title=title)
impose = False #request.args.get("impose") == "true"
return render_template('pagedjs.html', name=name.strip(), pad_content=html, lang=lang, title=title, cover=cover, impose=impose)
@APP.route('/<name>/imposed.html')
def imposed(name):
# TO GENERATE THE IMPOSED WEBPAGE
md_pad_content = get_pad_content(name, ext='.md')
try:
html = md_to_html(md_pad_content)
except MarkdownRenderError as exc:
return render_markdown_error(name, exc)
metadata = get_md_metadata(md_pad_content)
lang = get_meta(metadata, 'language', 'en')
title = get_meta(metadata, 'title', 'Untitled')
cover = get_meta(metadata, 'cover', None)
impose = True #request.args.get("impose") == "true"
return render_template('pagedjs.html', name=name.strip(), pad_content=html, lang=lang, title=title, cover=cover, impose=impose)
# //////////////////
if __name__ == '__main__':
APP.debug = True
APP.env = "development"
# APP.debug = False
# APP.env = "production"
APP.run(host="0.0.0.0", port=APP.config["PORTNUMBER"], threaded=True)

16
pad_filters.py Normal file
View file

@ -0,0 +1,16 @@
import re
from filter_registry import register_filter
@register_filter
def remove_star_before_img(text):
# regex version
# return re.sub(r'\*\s*!?\[\]\(', '![](', text)
return re.sub(r'\*\s*!?\[\.*]\(', '![](', text)
# @register_filter
# def remove_star_before_img(text):
# # print(">>> filter running")
# # print(">>> before:", repr(text))
# text = text.replace('*![](', '![](')
# # print(">>> after:", repr(text))
# return text

View file

@ -1,14 +1,17 @@
import os
from pathlib import Path
from dotenv import load_dotenv
# Load environment variables from the .env file
load_dotenv()
# Load environment variables from the .env file alongside this module.
ENV_PATH = Path(__file__).resolve().parent / ".env"
load_dotenv(dotenv_path=ENV_PATH)
# Bind them to Python variables
APPLICATION_ROOT = os.environ.get('OCTOMODE_APPLICATION_ROOT', '/')
PORTNUMBER = int(os.environ.get('OCTOMODE_PORTNUMBER', 5001))
PAD_URL = os.environ.get('OCTOMODE_PAD_URL', 'https://pad.vvvvvvaria.org')
PAD_API_URL = os.environ.get('OCTOMODE_PAD_API_URL', 'https://pad.vvvvvvaria.org/api/1.2.15')
HOME_PAD_URL = os.environ.get('OCTOMODE_HOME_PAD_URL', '')
PAD_API_KEY = os.environ.get('OCTOMODE_PAD_API_KEY', '')
# Check if API key is provided

49
static/footnotes.js Normal file
View file

@ -0,0 +1,49 @@
/*
Adds a custom css property that switches footnote style
between endnodes (default for Pandoc) and footnotes
modified from: https://pagedjs.org/plugins/endnotes-to-footnotes/
*/
const endNoteCalloutsQuery = ".footnote-ref";
// the hook
class endToFootNotes extends Paged.Handler {
constructor(chunker, polisher, caller) {
super(chunker, polisher, caller);
this.notestyle = 'endnotes';
}
onDeclaration(declaration, dItem, dList, rule) {
if (declaration.property == "--paged-note-style") {
if (declaration.value.value.includes("footnote")) {
console.log('FOOTNOTES!');
// console.log(declaration.property, declaration, rule);
this.notestyle = 'footnotes'
}
}
}
beforeParsed(content) {
if ('footnotes' !== this.notestyle) {
return;
}
console.log("parsef");
let callouts = content.querySelectorAll(endNoteCalloutsQuery);
callouts.forEach((callout) => {
console.log(callout.hash)
// console.log(callout.href)
// console.log(`#${callout.href.callout.href.hash}`)
let note = content.querySelector(callout.hash);
console.log(note);
if (!note) { return console.warn(`there is no note with the id of ${callout.hash}`) }
let noteContent = `<span class="pagedjs-end-to-footnote">${note.innerHTML}</span>`;
callout.insertAdjacentHTML("afterend", noteContent);
callout.remove();
note.remove();
});
}
}
Paged.registerHandlers(endToFootNotes);

428
static/imposition.js Normal file
View file

@ -0,0 +1,428 @@
// Imposition for booklet(s)
//
// This script re-arrange the pages of your document in order to make an imposed sheet layouts for printing.
// Two pages per sheet, double-sided
// modified from: https://gitlab.com/pagedjs-plugins/booklet-imposition/-/blob/master/imposition.js?ref_type=heads
class Booklet extends Paged.Handler {
constructor(chunker, polisher, caller) {
super(chunker, polisher, caller);
this.pagedbooklet;
this.sourceSize;
this.pageStart;
this.pageEnd;
}
onAtPage(node, item, list) { }
onDeclaration(declaration, dItem, dList, rule) {
if (declaration.property == "--paged-layout") {
if (declaration.value.value.includes("booklet")) {
console.log('BOOKLET');
// console.log(declaration.property, declaration, rule);
this.pagedbooklet = true;
let valuesBooklet = declaration.value.value.trim().split(/\s+/);
let index = valuesBooklet.indexOf("booklet");
/* Set first page of the imposition */
const rawStart = valuesBooklet[index + 1];
if (rawStart) {
const parsedStart = parseInt(rawStart, 10);
this.pageStart = Number.isNaN(parsedStart) ? 1 : parsedStart;
} else {
this.pageStart = 1;
}
/* Set last page of the imposition */
const rawEnd = valuesBooklet[index + 2];
if (rawEnd) {
const parsedEnd = parseInt(rawEnd, 10);
this.pageEnd = Number.isNaN(parsedEnd) ? undefined : parsedEnd;
} else {
this.pageEnd = undefined;
}
console.log(`START: ${this.pageStart} END: ${this.pageEnd}`);
}
}
}
afterRendered(pages) {
/* Verify this.pageEnd */
if (!this.pageEnd) {
let allPagesBefore = document.querySelectorAll(".pagedjs_page").length;
this.pageEnd = allPagesBefore;
}
/* Verify this.pageStart */
if (this.pageStart == 0) {
this.pageStart = 1;
} else if (this.pageStart % 2 == 0) {
this.pageStart = this.pageStart - 1;
}
/* Launch when printing */
//window.addEventListener("beforeprint", (evenement) => {
const reorder = (pages) => {
let containerPages = document.querySelector(".pagedjs_pages");
/* Delete pages we don't want*/
pages.forEach(page => {
let id = parseInt(page.id.replace('page-', ''));
if (id < this.pageStart || id > this.pageEnd) {
let pageSelect = document.querySelector('#' + page.id);
pageSelect.remove();
}
});
/* Reset page counter */
let reset = parseInt(this.pageStart) - 1;
containerPages.style.counterReset = "page " + reset;
let format = document.querySelector(".pagedjs_page");
/* Width of page without bleed, extract the first number of calc() function */
let width = getCSSCustomProp("--pagedjs-width", format);
let numbers = width
.match(/[0-9]+/g)
.map(function (n) {
return + (n);
});
width = parseInt(numbers[0]);
/* Height of page with bleed, addition of all the numbers of calc() function*/
let height = getCSSCustomProp("--pagedjs-height", format);
numbers = height
.match(/[0-9]+/g)
.map(function (n) {
return + (n);
});
const reducer = (previousValue, currentValue) => previousValue + currentValue;
height = numbers.reduce(reducer);
/* Bleed of the page */
let bleed = getCSSCustomProp("--pagedjs-bleed-top", format);
let bleedNum = parseInt(bleed);
/* Spread and half-spread*/
let spread = width * 2 + bleedNum * 2;
let spreadHalf = width + bleedNum;
// Add CSS to have pages in spread
//
// - change size of the page when printing (actually, sheet size)
// - flex properties
// - delete bleeds inside spread */
var newSize =
`@media print{
@page{
size: ${spread}mm ${height}mm;
}
.pagedjs_pages {
width: auto;
}
}
@media screen{
.pagedjs_pages{
max-width: calc(var(--pagedjs-width) * 2);
}
}
.pagedjs_pages {
display: flex !important;
flex-wrap: wrap;
transform: none !important;
height: 100% !important;
min-height: 100%;
max-height: 100%;
overflow: visible;
}
.pagedjs_page {
margin: 0;
padding: 0;
max-height: 100%;
min-height: 100%;
height: 100% !important;
}
.pagedjs_sheet {
margin: 0;
padding: 0;
max-height: 100%;
min-height: 100%;
height: 100% !important;
}
body{
--pagedjs-bleed-right-left: 0mm;
}
.pagedjs_left_page{
z-index: 20;
width: calc(var(--pagedjs-bleed-left) + var(--pagedjs-pagebox-width))!important;
}
.pagedjs_left_page .pagedjs_bleed-right .pagedjs_marks-crop {
border-color: transparent;
}
.pagedjs_right_page,
.pagedjs_right_page .pagedjs_sheet{
width: calc(var(--pagedjs-bleed-right-right) + var(--pagedjs-pagebox-width))!important;
}
.pagedjs_right_page .pagedjs_sheet{
grid-template-columns: [bleed-left] var(--pagedjs-bleed-right-left) [sheet-center] 1fr [bleed-right] var(--pagedjs-bleed-right-right);
}
.pagedjs_right_page .pagedjs_bleed-left{
display: none;
}
.pagedjs_right_page .pagedjs_bleed-top .pagedjs_marks-crop:nth-child(1),
.pagedjs_right_page .pagedjs_bleed-bottom .pagedjs_marks-crop:nth-child(1){
width: 0!important;
}
.pagedjs_first_page {
margin-left: 0;
}
body{
margin: 0
}
.pagedjs_page:nth-of-type(even){
break-after: always;
}
.pagedjs_page,
.pagedjs_sheet{
width: ${spreadHalf - 0.1}mm!important;
}
`;
// Add style for the arrangement of the pages
if (this.pagedbooklet == true) {
let style = document.createElement("style");
style.textContent = newSize;
document
.head
.appendChild(style);
var number_of_pages = document.getElementsByClassName("pagedjs_page").length;
var pages_array = [];
// If the page count isn't a multiple of 4, we need to pad the array with blank
// pages so we have the correct number of pages for a booklet.
//
// ex. [1, 2, 3, 4, 5, 6, 7, 8, 9, blank, blank, blank]
let modulo = number_of_pages % 4;
let additional_pages = 0;
if (modulo != 0) {
additional_pages = 4 - modulo;
}
for (i = 0; i < additional_pages; i++) {
let added_page = document.createElement("div");
added_page
.classList
.add("pagedjs_page", "added");
added_page.id = `page-${this.pageEnd + i + 1}`;
added_page.innerHTML = `
<div class="pagedjs_sheet">
<div class="pagedjs_bleed pagedjs_bleed-top">
<div class="pagedjs_marks-crop"></div>
<div class="pagedjs_marks-middle">
<div class="pagedjs_marks-cross"></div>
</div>
<div class="pagedjs_marks-crop"></div>
</div>
<div class="pagedjs_bleed pagedjs_bleed-bottom">
<div class="pagedjs_marks-crop"></div>
<div class="pagedjs_marks-middle">
<div class="pagedjs_marks-cross"></div>
</div> <div class="pagedjs_marks-crop"></div>
</div>
<div class="pagedjs_bleed pagedjs_bleed-left">
<div class="pagedjs_marks-crop"></div>
<div class="pagedjs_marks-middle">
<div class="pagedjs_marks-cross"></div>
</div> <div class="pagedjs_marks-crop"></div>
</div>
<div class="pagedjs_bleed pagedjs_bleed-right">
<div class="pagedjs_marks-crop"></div>
<div class="pagedjs_marks-middle">
<div class="pagedjs_marks-cross"></div>
</div>
<div class="pagedjs_marks-crop"></div>
</div>
<div class="pagedjs_pagebox">
<div class="pagedjs_margin-top-left-corner-holder">
<div class="pagedjs_margin pagedjs_margin-top-left-corner"><div class="pagedjs_margin-content"></div></div>
</div>
<div class="pagedjs_margin-top">
<div class="pagedjs_margin pagedjs_margin-top-left"><div class="pagedjs_margin-content"></div></div>
<div class="pagedjs_margin pagedjs_margin-top-center"><div class="pagedjs_margin-content"></div></div>
<div class="pagedjs_margin pagedjs_margin-top-right"><div class="pagedjs_margin-content"></div></div>
</div>
<div class="pagedjs_margin-top-right-corner-holder">
<div class="pagedjs_margin pagedjs_margin-top-right-corner"><div class="pagedjs_margin-content"></div></div>
</div>
<div class="pagedjs_margin-right">
<div class="pagedjs_margin pagedjs_margin-right-top"><div class="pagedjs_margin-content"></div></div>
<div class="pagedjs_margin pagedjs_margin-right-middle"><div class="pagedjs_margin-content"></div></div>
<div class="pagedjs_margin pagedjs_margin-right-bottom"><div class="pagedjs_margin-content"></div></div>
</div>
<div class="pagedjs_margin-left">
<div class="pagedjs_margin pagedjs_margin-left-top"><div class="pagedjs_margin-content"></div></div>
<div class="pagedjs_margin pagedjs_margin-left-middle"><div class="pagedjs_margin-content"></div></div>
<div class="pagedjs_margin pagedjs_margin-left-bottom"><div class="pagedjs_margin-content"></div></div>
</div>
<div class="pagedjs_margin-bottom-left-corner-holder">
<div class="pagedjs_margin pagedjs_margin-bottom-left-corner"><div class="pagedjs_margin-content"></div></div>
</div>
<div class="pagedjs_margin-bottom" style="grid-template-columns: 0px 1fr 0px;">
<div class="pagedjs_margin pagedjs_margin-bottom-left"><div class="pagedjs_margin-content"></div></div>
<div class="pagedjs_margin pagedjs_margin-bottom-center hasContent"><div class="pagedjs_margin-content"></div></div>
<div class="pagedjs_margin pagedjs_margin-bottom-right"><div class="pagedjs_margin-content"></div></div>
</div>
<div class="pagedjs_margin-bottom-right-corner-holder">
<div class="pagedjs_margin pagedjs_margin-bottom-right-corner"><div class="pagedjs_margin-content"></div></div>
</div>
<div class="pagedjs_area">
<div class="pagedjs_page_content"><div>
</div></div>
</div>
</div>
</div>
`;
document
.querySelector(".pagedjs_pages")
.appendChild(added_page);
}
// Push each page in the array
for (var i = number_of_pages + additional_pages; i >= 1; i--) {
pages_array.push(i);
}
// get the last page here, and add a 'pagedjs_last_page' class
// it's mixing functionality a bit, but i don't want to iterate
// a second time over the pages
const pagesList = Array.from(document.querySelectorAll('.pagedjs_page'));
pagesList.forEach(p => p.classList.remove('pagedjs_last_page'));
const last = pagesList.sort((a, b) => (parseInt(a.style.order || 0) - parseInt(b.style.order || 0))).pop();
console.log("last", last);
if (last) last.classList.add('pagedjs_last_page');
// Split the array in half
//
// ex. [1, 2, 3, 4, 5, 6], [7, 8, 9, blank, blank, blank]
var split_start = pages_array.length / 2;
var split_end = pages_array.length;
var first_array = pages_array.slice(0, split_start);
var second_array = pages_array.slice(split_start, split_end);
// Reverse the second half of the array. This is the beginning of the back half
// of the booklet (from the center fold, back to the outside last page)
//
// ex. [blank, blank, blank, 9, 8, 7]
var second_array_reversed = second_array.reverse();
// Zip the two arrays together in groups of 2 These will end up being each '2-up
// side' of the final document So, the sub-array at index zero will be the first
// side of physical page one and index 1 will be the back side. However, they
// won't yet be in the proper order.
//
// ex. [[1, blank], [2, blank], [3, blank], [4, 9], [5, 8], [6, 7]]
var page_groups = [];
for (var i = 0; i < first_array.length; i++) {
page_groups[i] = [
first_array[i], second_array_reversed[i]
];
}
// We need to reverse every other sub-array starting with the first side. This
// is the final step of aligning our booklet pages in the order with which the
// booklet gets printed and bound.
//
// ex. [[blank, 1], [2, blank], [blank, 3], [4, 9], [8, 5], [6, 7]] final_groups
// = page_groups.each_with_index { |group, index| group.reverse! if (index %
// 2).zero? }
var final_groups = [];
for (var i = 0; i < page_groups.length; i++) {
var group = page_groups[i];
if (i % 2 != 0) {
final_groups[i] = page_groups[i].reverse();
} else {
final_groups[i] = page_groups[i];
}
}
console.log("Final Imposition Order: " + final_groups);
var allPages = document.querySelectorAll(".pagedjs_page");
var final_flat = final_groups.flat();
final_flat.forEach((folio, i) => {
folio = folio + reset;
document
.querySelector(`#page-${folio}`)
.style
.order = i;
});
}
}; // before print
reorder(pages);
}
}
Paged
.registerHandlers(Booklet);
/**
* Pass in an element and its CSS Custom Property that you want the value of.
* Optionally, you can determine what datatype you get back.
*
* @param {String} propKey
* @param {HTMLELement} element=document.documentElement
* @param {String} castAs='string'
* @returns {*}
*/
const getCSSCustomProp = (
propKey,
element = document.documentElement,
castAs = "string"
) => {
let response = getComputedStyle(element).getPropertyValue(propKey);
// Tidy up the string if there's something to work with
if (response.length) {
response = response
.replace(/\'|"/g, "")
.trim();
}
// Convert the response into a whatever type we wanted
switch (castAs) {
case "number":
case "int":
return parseInt(response, 10);
case "float":
return parseFloat(response, 10);
case "boolean":
case "bool":
return response === "true" || response === "1";
}
// Return the string response by default
return response;
};

View file

@ -98,3 +98,25 @@ div.pagedjs_pages{
div#nav{
z-index: 11;
}
.cols {
display: grid;
grid-template-columns: 1fr 3fr;
margin-top: 3rem;
}
.home_pad_iframe {
height: 75vh;
/* margin: 5rem; */
/* width: calc(100vw - 13rem); */
border: 1px solid black;
border-radius: 5px;
}
@media only screen and (max-width: 600px) {
.home_pad_iframe {
margin: 1rem;
width: 90vw;
}
}

File diff suppressed because it is too large Load diff

View file

@ -34,7 +34,9 @@ window.addEventListener('load', function () {
<a href="{{ url_for('html', name=name) }}"><button>html</button></a>
<a href="{{ url_for('pdf', name=name) }}"><button>pdf</button></a>
<a href="{{ url_for('pdf', name=name) }}"><button>layout</button></a>
<a href="{{ url_for('impose', name=name) }}"><button>impose</button></a>
</div>`;
document.body.insertBefore(nav, document.body.firstChild);

View file

@ -2,34 +2,126 @@
@page{
size: A5;
margin: 10mm 20mm 25mm 20mm;
margin: 10mm 15mm 20mm 15mm;
@bottom-center{
content: counter(page);
font-family: monospace;
font-family: abordage;
font-size: 150%;
}
}
@page :first {
@bottom-center {
content: none;
}
}
@font-face {
font-family: abordage;
src: url(https://chatty-pub-files.hackersanddesigners.nl/files/2/99/nMPuQi6bsXWzzHQcKzByuHnk/abordage-regular.woff);
}
@font-face {
font-family: Director-Regular;
src: url(https://chatty-pub-files.hackersanddesigners.nl/files/2/d2/T7cPNlPHYfJD7uGerSbUl2zH/Director-Regular.otf);
}
@font-face {
font-family: Latitude;
src: url(https://chatty-pub-files.hackersanddesigners.nl/files/2/f8/52Q5ce2-rtPtoRRQpZrwp0X_/Latitude-Regular.otf);
}
body{
font-size: 12px;
font-family: Latitude;
font-size: 11px;
line-height: 1.5;
color: #822b01;
--paged-layout: booklet;
}
/* ------------------------------------ cover */
@page:first{
background-color: #f3c6ff;
color: #822b01;
@page:first {
color: white;
background-size: cover;
background-repeat: no-repeat;
filter: hue-rotate(17deg);
}
section#cover{
.cover{
break-after: always;
font-family: abordage;
margin-top: 50px;
padding: 15px;
background: #822b01;
background-clip: border-box;
}
section#cover h1#title{
font-size: 300%;
.cover h1#title {
font-family: abordage;
font-size: 500%;
line-height: 1.2em;
}
.cover h2 {
font-family: abordage;
font-size: 200%;
}
/* ------------------------------------ main */
section#main pre{
color: magenta;
h1 {
font-family: abordage;
font-size: 500%;
line-height: 1.2em;
}
section#main pre{
color: black;
}
.main h2 {
font-family: abordage;
font-size: 180%;
line-height: 1.2em;
}
blockquote{
margin-right: 0;
font-family: abordage;
font-size: 140%;
line-height: 1.2em;
}
/*
Images are always grayscale in the main content and
are alone on a page
*/
.main img {
display: block;
filter: grayscale(100%);
margin-left: auto;
margin-right: auto;
margin-top: 15px;
margin-bottom: 20px;
width: 100%;
page-break-after: always;
page-break-before: always;
}
/*
*** alone on a line in markdown will be turned into a <hr>
we use this a way to force a page break, and hide the hr itself.
*/
hr {
break-after: page;
border: none;
margin: 0;
height: 0;
}

View file

@ -5,75 +5,58 @@ language: en
<!--
|
__ __ _|_ __ _ _ _ __ __| _
/ \_/ | / \_/ |/ |/ | / \_/ | |/
\__/ \___/|_/\__/ | | |_/\__/ \_/|_/|__/
This document is opened in octomode.
Octomode is a collective editing space for PDF making that uses Etherpad, Paged.js, and Flask. It was first developed by Varia, building on the knowledge and practices of many designers and developers who work with and contribute to open-source and web-to-print approaches to making publications.
Please keep in mind:
This document is opened in octomode.
* This instance of octomode is hosted on the server of Hackers & Designer's and is subject to H&D's code of conduct: https://hackersanddesigners.nl/code-of-conduct
pad : all materials for the PDF are collected here (written in Markdown)
stylesheet : all CSS rules for the PDF are collected here (written in CSS)
html : render the structure of the lay out as a HTML (with PyPandoc)
[note] this view does not render any styling!
pdf : render the lay out as a PDF (with Paged.js)
* The pads are not indexed by search engines, but anyone who knows the URL can read and change them.
https://git.vvvvvvaria.org/varia/octomode
* The contents of the pads are not encrypted, meaning that they are not private.
-->
<!--
* We make our own backups, meaning the the contents of all pads sit on H&D's harddrives potentially indefinitely.
v _____ v _____ _ _ v _____ v ____ ____ _ ____
\| ___"|/ |_ " _| |'| |'| \| ___"|/v | _"\ v v| _"\ vv /"\ v | _"\
| _|" V | | /| |_| |\ | _|" R \| |_) |/ \| |_) |/ \/ _ \/ /| | | |
| |___ /| |\ v| A |v | |___ | _ < I | __/ / ___ \ v| |_| |\
|_____| v |_|v |_| |_| |_____| |_| \_\ |_| A/_/ \_\ |____/ v
<< >> _// \\_ // \\ << >> // \\_ ||>>_ \\ >> |||_
(_V_) (_A_)(_R_) (_I_)(_A_) ("_)(__) (__) (__) (_P_)(_A_)_D_) (__) (__)
* We might not be able to respond to pad retrieval requests.
Welcome to the etherpad-lite instance hosted by Varia!
You are most welcome to use it but please take note of the following things:
* H&D is a small collective with changing energies and availabilities, which might affect the availability of the pads
VISIBILITY:
- The pads are not indexed by search engines, but anyone that knows its URL is welcome to read and edit it.
* If you rely on the content of these pads, please remember to make your own backups.
PRIVACY:
- The contents of the pads are not encrypted, meaning that they are not private.
- Anyone with access to the server has the possibility to see the content of your pads.
RETENTION:
- We make our own backups, meaning the the contents of all pads sit on our harddrives potentially indefinitely.
- Because the identity of a pad author cannot be confirmed, we don't respond to pad retrieval requests.
Octomode is for everyone who enjoys working side by side and looks for ways to escape, divest, and boycott big tech companies that create unaffordable, inaccessible black box software and are complicit in the genocidal war on Palestine.
ACCESSIBILITY:
- If you rely on the content of these pads, please remember to make your own backups.
- The availability of the pads is subject to cosmic events, spilled drinks and personal energies.
CODE OF CONDUCT:
- Both the physical and digital spaces of Varia are subject to our Code of Conduct <https://varia.zone/en/pages/code-of-conduct.html>
If you wish to publish a pad to the Varia etherdump <https://etherdump.vvvvvvaria.org/> add the magic word __ PUBLISH __ (remove the spaces between the word and __) to your pad.
How it works:
pad: all content for the PDF are collected here (written in Markdown)
stylesheet: all CSS rules for the PDF are collected here (written in CSS)
html: render the structure of the layout as a HTML (with PyPandoc)
layout: render the layout as a PDF, single pages (with Paged.js)
imposition: sort pages so they can be printed double sided, folded in the middle an stapled.
#MD?: markdown cheatsheet
https://git.vvvvvvaria.org/varia/octomode
-->
<section id="cover">
# *in octomode* { #title }
::: cover
# Title
## Author
Everything else that goes on the cover
:::
::: main
</section>
# Title
## Author
<section id="main">
Octomode is a collective editing space for PDF making, using Etherpad, Paged.js and Flask.
Everything that goes into the zine
Inspired by the non-centralised, tentacular cognition capabilities of the octopus, we imagined a space in which the artificial boundaries of writing and designing can be crossed; where writing, editing and designing can be done in one environment simultaneously, allowing the format to influence the matter and vice-versa.
:::
```
Edit this text in the PAD view.
Edit the styling in the STYLESHEET view.
Preview the page in the HTML view.
Render it on pages in the PDF view.
```
</section>

35
templates/error.html Normal file
View file

@ -0,0 +1,35 @@
<!DOCTYPE html>
<html lang="{{ lang }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="{{ url_for('css', name=name) }}" rel="stylesheet" type="text/css" media="print">
<title>{{ title }} - {{ name }}</title>
</head>
<body>
<h1>{{ title }}</h1>
<p>The document "{{name}}" has an error.</p>
{% if error.kind == "yaml" %}
<p>It seems like that there's something wrong with the YAML at the top of your document. <br />
It should be structured like this:
</p>
<pre>
---
title: My Title
lang: en
cover: https://example.com/image.jpg
---
</pre>
<p>
Make sure that there is a space between the ":" and the value.
</p>
{% endif %}
{% elif error.kind == "markdown" %}
There's something wrong with the markdown in your pad. Please check the document or revert some of your recent changes.
{% endif %}
<details>
<summary>Details</summary>
{{error.original}}
</details>
</body>
</html>

View file

@ -4,8 +4,22 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="{{ url_for('static', filename='paged.polyfill.js') }}" type="text/javascript"></script>
<script src="{{ url_for('static', filename='footnotes.js') }}" type="text/javascript"></script>
{% if impose %}
<script src="{{ url_for('static', filename='imposition.js') }}" type="text/javascript" id="imposition_js"></script>
<style>body{
--paged-layout: booklet;
}</style>
{% endif %}
{% if cover %}
<style>
.pagedjs_first_page {
background-image: url({{cover}});
}
</style>
{% endif %}
<link href="{{ url_for('static', filename='pagedjs.css') }}" rel="stylesheet" type="text/css" media="screen">
<link href="/{{ name }}/stylesheet.css" rel="stylesheet" type="text/css" media="print">
<link href="{{ url_for('css', name=name) }}" rel="stylesheet" type="text/css" media="print">
<title>{{ title }}</title>
</head>
<body>

View file

@ -1,5 +1,9 @@
{% extends "base.html" %}
{% block head %}
<title>hallo</title>
{% endblock %}
{% block content %}
<iframe id="pdf" name="pdf" src="{{ url }}"></iframe>
{% endblock %}
@ -20,9 +24,12 @@ window.addEventListener('load', function () {
var head = document.getElementsByTagName('head')[0];
head.insertBefore(cssLink, head.firstChild);
// Insert the SAVE button
const nav = document.getElementById('buttons');
const save = '<a href="#"><button id="save" onClick="printPage()">save</button></a>';
// insert the IMPOSE button
// const impose = '<a href="#"><button id="impose" onClick="impose()">impose</button></a>';
// Insert the SAVE button
const save = '<a href="#"><button id="save" onClick="printPage()" style="background-color: #66ee66;">save</button></a>';
nav.innerHTML = nav.innerHTML + save;
})

View file

@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="/{{ name }}/stylesheet.css" rel="stylesheet" type="text/css" media="screen">
<link href="{{ url_for('css', name=name) }}" rel="stylesheet" type="text/css" media="screen">
<title>{{ title }}</title>
</head>
<body>

View file

@ -4,10 +4,24 @@
<meta charset="utf-8" />
<title>octomode</title>
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='main.css') }}">
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<style>
</style>
</head>
<body class="start-page">
<form action="{{ url_for('index') }}" method="POST">
<h1><input type="submit" value="open"> <input type="text" name="name"> <em class="octomode">in octomode</em></h1>
</form>
<div class="cols">
<h2>Publications</h2>
<p>Below a list of the publications on the server.</p>
<ul>
{% for pub in pubs %}
<a href="{{ url_for('pdf',name=pub)}}">{{pub}}</a>
{% endfor %}
</ul>
<iframe class="home_pad_iframe" src="{{ home_pad_url }}"></iframe>
</div>
</body>
</html>