import changes from lepa

This commit is contained in:
Heerko 2025-12-27 14:51:12 +01:00
parent 378f37c9b9
commit ef640752cc
15 changed files with 780 additions and 83 deletions

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 flask import Flask, request, render_template, redirect, url_for
from urllib.request import urlopen from urllib.request import urlopen
from urllib.parse import urlencode from urllib.parse import urlencode
import html
# To sanitize Flask input fields # To sanitize Flask input fields
from markupsafe import Markup, escape from markupsafe import Markup, escape
@ -14,6 +15,11 @@ import pypandoc
# To read the Markdown metadat # To read the Markdown metadat
import markdown import markdown
from filter_registry import pad_content_filters
import pad_filters
APP = Flask(__name__) APP = Flask(__name__)
APP.config.from_pyfile('settings.py') APP.config.from_pyfile('settings.py')
@ -40,6 +46,11 @@ def get_pad_content(pad_name, ext=""):
response = json.load(urlopen(f"{ APP.config['PAD_API_URL'] }/{ api_call }", data=urlencode(arguments).encode())) response = json.load(urlopen(f"{ APP.config['PAD_API_URL'] }/{ api_call }", data=urlencode(arguments).encode()))
content = response['data']['text'] 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 return content
def all_pads(): def all_pads():
@ -95,6 +106,25 @@ def get_md_metadata(md_pad_content):
return metadata 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]
# --- # ---
@APP.route('/', methods=['GET', 'POST']) @APP.route('/', methods=['GET', 'POST'])
@ -110,7 +140,7 @@ def index():
create_pad_on_first_run(name, ext) create_pad_on_first_run(name, ext)
return redirect(url_for("pad", name=name)) return redirect(url_for("pad", name=name))
else: else:
return render_template('start.html', pad_url=APP.config['PAD_URL']) return render_template('start.html', home_pad_url=APP.config['HOME_PAD_URL'])
@APP.route('/<name>/') @APP.route('/<name>/')
def main(name): def main(name):
@ -128,28 +158,22 @@ def stylesheet(name):
@APP.route('/<name>/html/') @APP.route('/<name>/html/')
def html(name): def html(name):
# only here we need application root to make all the URLs work..... app_root = 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']
url = f"{ app_root }/{ name }/preview.html" url = f"{ app_root }/{ name }/preview.html"
return render_template('iframe.html', url=url, name=name.strip(), pad_url=APP.config['PAD_URL']) return render_template('iframe.html', url=url, name=name.strip(), pad_url=APP.config['PAD_URL'])
@APP.route('/<name>/pdf/') @APP.route('/<name>/pdf/')
def pdf(name): def pdf(name):
# only here we need application root to make all the URLs work..... app_root = 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']
url = f"{ app_root }/{name}/pagedjs.html" url = f"{ app_root }/{name}/pagedjs.html"
return render_template('pdf.html', url=url, name=name.strip(), pad_url=APP.config['PAD_URL']) 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 # RENDERED RESOURCES
# ////////////////// # //////////////////
@ -168,12 +192,8 @@ def preview(name):
md_pad_content = get_pad_content(name, ext='.md') md_pad_content = get_pad_content(name, ext='.md')
html = md_to_html(md_pad_content) html = md_to_html(md_pad_content)
metadata = get_md_metadata(md_pad_content) metadata = get_md_metadata(md_pad_content)
if metadata: lang = get_meta(metadata, 'language', 'en')
lang = metadata['language'][0] title = get_meta(metadata, 'title', 'Untitled')
title = metadata['title'][0]
else:
lang = "en"
title = "No title"
return render_template('preview.html', name=name.strip(), pad_content=html, lang=lang, title=title) return render_template('preview.html', name=name.strip(), pad_content=html, lang=lang, title=title)
@ -183,10 +203,26 @@ def pagedjs(name):
md_pad_content = get_pad_content(name, ext='.md') md_pad_content = get_pad_content(name, ext='.md')
html = md_to_html(md_pad_content) html = md_to_html(md_pad_content)
metadata = get_md_metadata(md_pad_content) metadata = get_md_metadata(md_pad_content)
lang = metadata['language'][0] lang = get_meta(metadata, 'language', 'en')
title = metadata['title'][0] 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 PAGED.JS WEBPAGE
md_pad_content = get_pad_content(name, ext='.md')
html = md_to_html(md_pad_content)
metadata = get_md_metadata(md_pad_content)
lang = get_meta(metadata, 'language', 'en')
title = get_meta(metadata, 'title', 'Untitled')
impose = True #request.args.get("impose") == "true"
return render_template('pagedjs.html', name=name.strip(), pad_content=html, lang=lang, title=title, impose=impose)
# ////////////////// # //////////////////

15
pad_filters.py Normal file
View file

@ -0,0 +1,15 @@
import re
from filter_registry import register_filter
@register_filter
def remove_star_before_img(text):
# regex version
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

@ -9,7 +9,8 @@ APPLICATION_ROOT = os.environ.get('OCTOMODE_APPLICATION_ROOT', '/')
PORTNUMBER = int(os.environ.get('OCTOMODE_PORTNUMBER', 5001)) PORTNUMBER = int(os.environ.get('OCTOMODE_PORTNUMBER', 5001))
PAD_URL = os.environ.get('OCTOMODE_PAD_URL', 'https://pad.vvvvvvaria.org') 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') PAD_API_URL = os.environ.get('OCTOMODE_PAD_API_URL', 'https://pad.vvvvvvaria.org/api/1.2.15')
PAD_API_KEY = os.environ.get('OCTOMODE_PAD_API_KEY', '') PAD_API_KEY = "97e3bf6a626eaaa3426833db3d936f3a"
#os.environ.get('OCTOMODE_PAD_API_KEY', '')
# Check if API key is provided # Check if API key is provided
if not PAD_API_KEY or PAD_API_KEY == "XXX": if not PAD_API_KEY or PAD_API_KEY == "XXX":

55
static/footnotes.js Normal file
View file

@ -0,0 +1,55 @@
/*
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")) {
this.notestyle = 'footnotes';
}
}
}
beforeParsed(content) {
if (this.notestyle !== 'footnotes') return;
// Clean up previously injected clones (in case of re-render).
content.querySelectorAll('.pagedjs-footnote-clone').forEach((n) => n.remove());
// Optional: hide the original footnote list; we will clone notes inline.
const originalList = content.querySelector('.footnotes');
if (originalList) {
originalList.classList.add('pagedjs-footnotes-hidden');
}
const callouts = content.querySelectorAll(endNoteCalloutsQuery);
callouts.forEach((callout) => {
if (!callout.hash) return;
const note = content.querySelector(callout.hash);
if (!note) {
console.warn(`No footnote found for ${callout.hash}`);
return;
}
// Clone the note content and float it as a footnote.
const clone = document.createElement('span');
clone.classList.add('pagedjs-footnote-clone');
clone.innerHTML = note.innerHTML;
callout.insertAdjacentElement('afterend', clone);
});
}
}
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,19 @@ div.pagedjs_pages{
div#nav{ div#nav{
z-index: 11; z-index: 11;
} }
.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;
}
}

View file

@ -178,3 +178,14 @@
.pagedjs_bleed-right .pagedjs_marks-crop:last-child{ .pagedjs_bleed-right .pagedjs_marks-crop:last-child{
box-shadow: 0px -1px 0px 0px var(--pagedjs-crop-shadow); box-shadow: 0px -1px 0px 0px var(--pagedjs-crop-shadow);
} }
/* Footnotes: clone definitions inline and float them to the footer */
/* .pagedjs-footnote-clone {
float: footnote;
display: block;
margin: 0;
padding: 0;
}
.pagedjs-footnotes-hidden {
display: none;
} */

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('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>`; </div>`;
document.body.insertBefore(nav, document.body.firstChild); document.body.insertBefore(nav, document.body.firstChild);

View file

@ -2,34 +2,126 @@
@page{ @page{
size: A5; size: A5;
margin: 10mm 20mm 25mm 20mm; margin: 10mm 15mm 20mm 15mm;
@bottom-center{ @bottom-center{
content: counter(page); 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{ body{
font-size: 12px; font-family: Latitude;
font-size: 11px;
line-height: 1.5; line-height: 1.5;
color: #822b01; color: #822b01;
--paged-layout: booklet;
} }
/* ------------------------------------ cover */ /* ------------------------------------ cover */
@page:first{ @page:first {
background-color: #f3c6ff; color: white;
color: #822b01; background-size: cover;
background-repeat: no-repeat;
filter: hue-rotate(17deg);
} }
section#cover{
.cover{
break-after: always; 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 */ /* ------------------------------------ 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,56 +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) * The pads are not indexed by search engines, but anyone who knows the URL can read and change them.
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)
https://git.vvvvvvaria.org/cc/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.
* We might not be able to respond to pad retrieval requests.
* H&D is a small collective with changing energies and availabilities, which might affect the availability of the pads
* If you rely on the content of these pads, please remember to make your own backups.
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.
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
--> -->
<!--
------------------------------------------------------------------------------- ::: cover
This pad is hosted by CC (creative crowds), a server that we share for doing
collective research on the entanglements of tools, cultures and infrastructure.
How can this server be available AND unstable, public AND being paid for, # Title
free to be used AND situated, a production environment AND in transformation? ## Author
While surfing the contradictions, we are formulting collective guidelines for Everything else that goes on the cover
engaging with this server, which you can find at: https://cc.practices.tools
-------------------------------------------------------------------------------
--> :::
<section id="cover">
# *in octomode* { #title }
::: main
</section> # Title
## Author
<section id="main"> Everything that goes into the zine
Octomode is a collective editing space for PDF making, using Etherpad, Paged.js and Flask.
Inspired by the multi-centered, tentacular cognition capabilities of the octopus, we imagined a space in which the artificial boundaries of writing and design 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>

View file

@ -4,9 +4,22 @@
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <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='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="{{ url_for('static', filename='pagedjs.css') }}" rel="stylesheet" type="text/css" media="screen">
<!-- [LOCAL HACK] mb: added /octomode/ below to make the stylesheet link work again. --> <link href="{{ url_for('css', name=name) }}" rel="stylesheet" type="text/css" media="print">
<link href="/octomode/{{ name }}/stylesheet.css" rel="stylesheet" type="text/css" media="print">
<title>{{ title }}</title> <title>{{ title }}</title>
</head> </head>
<body> <body>

View file

@ -1,5 +1,9 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block head %}
<title>hallo</title>
{% endblock %}
{% block content %} {% block content %}
<iframe id="pdf" name="pdf" src="{{ url }}"></iframe> <iframe id="pdf" name="pdf" src="{{ url }}"></iframe>
{% endblock %} {% endblock %}
@ -20,9 +24,12 @@ window.addEventListener('load', function () {
var head = document.getElementsByTagName('head')[0]; var head = document.getElementsByTagName('head')[0];
head.insertBefore(cssLink, head.firstChild); head.insertBefore(cssLink, head.firstChild);
// Insert the SAVE button
const nav = document.getElementById('buttons'); 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; nav.innerHTML = nav.innerHTML + save;
}) })

View file

@ -3,8 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- [LOCAL HACK] mb: added /octomode/ below to make the stylesheet link work again. --> <link href="{{ url_for('css', name=name) }}" rel="stylesheet" type="text/css" media="screen">
<link href="/octomode/{{ name }}/stylesheet.css" rel="stylesheet" type="text/css" media="screen">
<title>{{ title }}</title> <title>{{ title }}</title>
</head> </head>
<body> <body>

View file

@ -4,10 +4,12 @@
<meta charset="utf-8" /> <meta charset="utf-8" />
<title>octomode</title> <title>octomode</title>
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='main.css') }}"> <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='main.css') }}">
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
</head> </head>
<body class="start-page"> <body class="start-page">
<form action="{{ url_for('index') }}" method="POST"> <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> <h1><input type="submit" value="open"> <input type="text" name="name"> <em class="octomode">in octomode</em></h1>
</form> </form>
<iframe class="home_pad_iframe" src="{{ home_pad_url }}"></iframe>
</body> </body>
</html> </html>