...
This commit is contained in:
parent
888b2372fa
commit
473c48689d
18 changed files with 965 additions and 2 deletions
BIN
tools/a5-reference.odt
Normal file
BIN
tools/a5-reference.odt
Normal file
Binary file not shown.
58
tools/autowidth.lua
Normal file
58
tools/autowidth.lua
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
-- Verteilt Tabellenspalten proportional zur Inhaltslänge (statt gleicher
|
||||
-- Breiten aus der Markdown-Trennzeile). Für Tabellen mit langer
|
||||
-- Beschreibungsspalte (typisch bei RPG-Tabellen) deutlich besser lesbar.
|
||||
--
|
||||
-- Die Breite einer Spalte = max. Zeichenlänge ihrer Zellen (Header + Rumpf),
|
||||
-- normalisiert auf Anteile. Mindestbreite 12 %, um schmale Kennspalten
|
||||
-- (z. B. "d6") nicht zu erdrücken.
|
||||
|
||||
local FLOOR = 0.08
|
||||
|
||||
local function strlen(s)
|
||||
local ok, n = pcall(utf8.len, s)
|
||||
if ok then return n end
|
||||
return #s
|
||||
end
|
||||
|
||||
local function celltext(blocks)
|
||||
return pandoc.utils.stringify(blocks)
|
||||
end
|
||||
|
||||
function Table(tbl)
|
||||
local ncols = #tbl.colspecs
|
||||
if ncols < 1 then return tbl end
|
||||
|
||||
local lens = {}
|
||||
for i = 1, ncols do lens[i] = 1 end
|
||||
|
||||
local function measure(cells)
|
||||
for i, cell in ipairs(cells) do
|
||||
local l = strlen(celltext(cell.contents))
|
||||
if l > lens[i] then lens[i] = l end
|
||||
end
|
||||
end
|
||||
|
||||
if tbl.head and tbl.head.rows then
|
||||
for _, row in ipairs(tbl.head.rows) do measure(row.cells) end
|
||||
end
|
||||
for _, body in ipairs(tbl.bodies) do
|
||||
for _, row in ipairs(body.body) do measure(row.cells) end
|
||||
end
|
||||
|
||||
local total = 0
|
||||
for i = 1, ncols do
|
||||
lens[i] = math.sqrt(lens[i])
|
||||
total = total + lens[i]
|
||||
end
|
||||
|
||||
if total > 0 then
|
||||
for i = 1, ncols do
|
||||
local w = lens[i] / total
|
||||
if w < FLOOR then w = FLOOR end
|
||||
-- ColSpec ist positionsbasiert: [1]=align, [2]=width
|
||||
tbl.colspecs[i][2] = w
|
||||
end
|
||||
end
|
||||
|
||||
return tbl
|
||||
end
|
||||
69
tools/make_reference_odt.py
Executable file
69
tools/make_reference_odt.py
Executable file
|
|
@ -0,0 +1,69 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Erzeugt tools/a5-reference.odt aus der Pandoc-Standardreferenz,
|
||||
angepasst an dein A5-Layout:
|
||||
|
||||
- A5 (14,801 x 21,001 cm), Ränder oben 0,8 / unten 1,0 / links 1,0 / rechts 0,7 cm
|
||||
- Grundschrift Alegreya 10 pt, Überschriften Cinzel
|
||||
- keine Seitennummern (Footer entfernt)
|
||||
|
||||
Aufruf: python3 make_reference_odt.py
|
||||
"""
|
||||
import io
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import zipfile
|
||||
|
||||
TOOLS = os.path.dirname(os.path.abspath(__file__))
|
||||
OUT = os.path.join(TOOLS, "a5-reference.odt")
|
||||
|
||||
# 1) Pandoc-Standardreferenz holen
|
||||
default = subprocess.check_output(
|
||||
["pandoc", "--print-default-data-file", "reference.odt"]
|
||||
)
|
||||
|
||||
zin = zipfile.ZipFile(io.BytesIO(default))
|
||||
names = zin.namelist()
|
||||
items = {n: zin.read(n) for n in names}
|
||||
|
||||
styles = items["styles.xml"].decode("utf-8")
|
||||
|
||||
# 2) Gezielte Ersetzungen
|
||||
REPL = [
|
||||
# --- Seite A5 + Ränder ---
|
||||
('fo:page-width="8.5in"', 'fo:page-width="14.801cm"'),
|
||||
('fo:page-height="11in"', 'fo:page-height="21.001cm"'),
|
||||
('fo:margin-top="1in"', 'fo:margin-top="0.8cm"'),
|
||||
('fo:margin-bottom="1in"', 'fo:margin-bottom="1cm"'),
|
||||
('fo:margin-left="1in"', 'fo:margin-left="1cm"'),
|
||||
('fo:margin-right="1in"', 'fo:margin-right="0.7cm"'),
|
||||
# --- Schriften ---
|
||||
("Times New Roman", "Alegreya"),
|
||||
("Arial", "Cinzel"),
|
||||
("Lucida Sans Unicode", "Cinzel"),
|
||||
("Tahoma1", "Cinzel"),
|
||||
("Tahoma", "Cinzel"),
|
||||
# --- Grundschrift 12 pt -> 10 pt ---
|
||||
('fo:font-size="12pt"', 'fo:font-size="10pt"'),
|
||||
('font-size-asian="12pt"', 'font-size-asian="10pt"'),
|
||||
('font-size-complex="12pt"', 'font-size-complex="10pt"'),
|
||||
]
|
||||
|
||||
for a, b in REPL:
|
||||
styles = styles.replace(a, b)
|
||||
|
||||
# 3) Footer (Seitennummer) aus dem Master-Page entfernen
|
||||
styles = re.sub(r"<style:footer>.*?</style:footer>", "", styles, flags=re.S)
|
||||
|
||||
items["styles.xml"] = styles.encode("utf-8")
|
||||
|
||||
# 4) Neu packen: mimetype zuerst und unkomprimiert
|
||||
with zipfile.ZipFile(OUT, "w") as z:
|
||||
z.writestr("mimetype", items["mimetype"], compress_type=zipfile.ZIP_STORED)
|
||||
for n in names:
|
||||
if n == "mimetype":
|
||||
continue
|
||||
z.writestr(n, items[n], compress_type=zipfile.ZIP_DEFLATED)
|
||||
|
||||
print("OK:", OUT)
|
||||
42
tools/md2odt.sh
Executable file
42
tools/md2odt.sh
Executable file
|
|
@ -0,0 +1,42 @@
|
|||
#!/usr/bin/env bash
|
||||
#
|
||||
# Markdown (Obsidian) -> editierbares LibreOffice-ODT (A5)
|
||||
#
|
||||
# Aufruf:
|
||||
# ./tools/md2odt.sh <datei.md> [ausgabename-ohne-endung]
|
||||
#
|
||||
# Ausgabe (Standard): Druckmaterial/<name>.odt
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
VAULT="$ROOT/RPG"
|
||||
TOOLS="$ROOT/tools"
|
||||
OUTDIR="${OUTDIR:-$ROOT/Druckmaterial/autogen_inbox}"
|
||||
|
||||
if [ "$#" -lt 1 ]; then
|
||||
echo "Aufruf: $0 <datei.md> [ausgabename]" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SRC="$(realpath "$1")"
|
||||
NAME="${2:-$(basename "$SRC" .md)}"
|
||||
OUT="$OUTDIR/${NAME}.odt"
|
||||
|
||||
mkdir -p "$OUTDIR"
|
||||
|
||||
TMP="$(mktemp -d)"
|
||||
trap 'rm -rf "$TMP"' EXIT
|
||||
|
||||
# 1) Obsidian-Syntax vorverarbeiten
|
||||
python3 "$TOOLS/obsidian_preprocess.py" "$VAULT" < "$SRC" > "$TMP/in.md"
|
||||
|
||||
# 2) Markdown -> ODT (A5-Referenzvorlage; H1 bleibt als "Heading 1" erhalten)
|
||||
pandoc "$TMP/in.md" \
|
||||
-f markdown+raw_html \
|
||||
-t odt \
|
||||
--reference-doc="$TOOLS/a5-reference.odt" \
|
||||
--lua-filter="$TOOLS/autowidth.lua" \
|
||||
-o "$OUT"
|
||||
|
||||
echo "OK: $OUT"
|
||||
116
tools/obsidian_preprocess.py
Normal file
116
tools/obsidian_preprocess.py
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Vorverarbeitung von Obsidian-Markdown zu sauberem Pandoc-Markdown.
|
||||
|
||||
Behandelt:
|
||||
- Obsidian-Wikilinks: [[Ziel]] [[Ziel|Anzeige]] [[bild.jpg|BreitePx]]
|
||||
- Emoji -> drucktaugliche Glyphen bzw. Text (dekorative Emojis werden entfernt)
|
||||
- ==Markierung== -> **fett** (drucktauglicher Ersatz)
|
||||
- %% Kommentare %% -> entfernt
|
||||
- Variation-Selektoren (U+FE0F/U+FE0E) entfernen
|
||||
|
||||
Aufruf:
|
||||
obsidian_preprocess.py <VAULT_ROOT> < eingabe.md > ausgabe.md
|
||||
"""
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
IMG_EXT = {".jpg", ".jpeg", ".png", ".webp", ".gif", ".bmp", ".svg", ".avif"}
|
||||
|
||||
# Emoji -> Ersatz (leerer String = entfernen)
|
||||
EMOJI = {
|
||||
"\U0001F449": "\u2192", # 👉 ->
|
||||
"\U0001F7E1": "\u25CF", # 🟡 ●
|
||||
"\u26A0": "\u26A0", # ⚠ (wird über DejaVu-Fallback gesetzt)
|
||||
"\u2600": "\u2600", # ☀ (DejaVu-Fallback)
|
||||
"\U0001F338": "", # 🌸
|
||||
"\U0001F342": "", # 🍂
|
||||
"\u2744": "", # ❄
|
||||
"\U0001F324": "", # 🌤
|
||||
"\U0001F5FA": "", # 🗺
|
||||
"\U0001F3D4": "", # 🏔
|
||||
"\U0001F5E3": "", # 🗣
|
||||
"\U0001F48E": "", # 💎
|
||||
"\U0001F3B2": "", # 🎲
|
||||
"\u26A1": "", # ⚡
|
||||
"\U0001F4C8": "", # 📈
|
||||
"\U0001F4C9": "", # 📉
|
||||
"\U0001F504": "", # 🔄
|
||||
}
|
||||
VS = {"\uFE0F", "\uFE0E"}
|
||||
|
||||
SKIP_DIRS = {".git", ".obsidian", ".trash", "node_modules"}
|
||||
|
||||
|
||||
def scan_images(root: str) -> dict:
|
||||
"""basename(lower) -> absoluter Pfad (nur Nicht-.trash-Pfade)."""
|
||||
mapping = {}
|
||||
|
||||
def walk():
|
||||
for dirpath, dirnames, filenames in os.walk(root):
|
||||
dirnames[:] = [d for d in dirnames if d not in SKIP_DIRS]
|
||||
for fn in filenames:
|
||||
if os.path.splitext(fn)[1].lower() in IMG_EXT:
|
||||
# Absoluter Pfad, damit Pandoc das Bild unabhängig vom
|
||||
# aktuellen Arbeitsverzeichnis findet.
|
||||
full = os.path.abspath(os.path.join(dirpath, fn))
|
||||
key = fn.lower()
|
||||
mapping.setdefault(key, full)
|
||||
walk()
|
||||
return mapping
|
||||
|
||||
|
||||
def px_to_cm(px: float) -> float:
|
||||
return round(px / 96.0 * 2.54, 2)
|
||||
|
||||
|
||||
def resolve_wikilink(image_map: dict, inner: str) -> str:
|
||||
if "|" in inner:
|
||||
target, disp = inner.split("|", 1)
|
||||
else:
|
||||
target, disp = inner, None
|
||||
target = target.strip()
|
||||
ext = os.path.splitext(target.lower())[1]
|
||||
if ext in IMG_EXT:
|
||||
path = image_map.get(target.lower(), target)
|
||||
alt = os.path.basename(target)
|
||||
if disp is not None and disp.strip().isdigit():
|
||||
width = px_to_cm(int(disp.strip()))
|
||||
return f"{{width={width}cm}}"
|
||||
return f"{{width=100%}}"
|
||||
# Notiz-Link -> Klartext
|
||||
return (disp.strip() if disp is not None else target)
|
||||
|
||||
|
||||
def process(text: str, image_map: dict) -> str:
|
||||
# 1. Kommentare
|
||||
text = re.sub(r"%%.*?%%", "", text, flags=re.DOTALL)
|
||||
# 2. Wikilinks
|
||||
text = re.sub(r"\[\[([^\]]+)\]\]", lambda m: resolve_wikilink(image_map, m.group(1)), text)
|
||||
# 3. Variation-Selektoren
|
||||
for vs in VS:
|
||||
text = text.replace(vs, "")
|
||||
# 4. Emoji
|
||||
for k, v in EMOJI.items():
|
||||
text = text.replace(k, v)
|
||||
# 5. ==mark== -> **fett**
|
||||
text = re.sub(r"==([^=\n]+)==", r"**\1**", text)
|
||||
# 6. Leerraum glätten (nach Emoji-Entfernung), aber Tabellen/Code unberührt
|
||||
out = []
|
||||
for line in text.split("\n"):
|
||||
stripped = line.lstrip()
|
||||
if not stripped.startswith("|") and not stripped.startswith("```"):
|
||||
line = re.sub(r"[ \t]{2,}", " ", line)
|
||||
out.append(line)
|
||||
return "\n".join(out)
|
||||
|
||||
|
||||
def main():
|
||||
root = sys.argv[1] if len(sys.argv) > 1 else "."
|
||||
image_map = scan_images(root)
|
||||
sys.stdout.write(process(sys.stdin.read(), image_map))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
Add table
Add a link
Reference in a new issue