From 0d81f3fa3b7150f51c08ce2727dd9af56994b49a Mon Sep 17 00:00:00 2001 From: stev Date: Thu, 31 Aug 2023 18:55:27 +0200 Subject: [PATCH] initial commit --- README.md | 22 +++++ gmirator/__init__.py | 0 gmirator/generate.py | 171 +++++++++++++++++++++++++++++++++ gmirator/main.py | 14 +++ gmirator/synchronise.py | 33 +++++++ gmirator/test/__init__.py | 0 gmirator/test/test_generate.py | 5 + 7 files changed, 245 insertions(+) create mode 100644 README.md create mode 100644 gmirator/__init__.py create mode 100644 gmirator/generate.py create mode 100644 gmirator/main.py create mode 100644 gmirator/synchronise.py create mode 100644 gmirator/test/__init__.py create mode 100644 gmirator/test/test_generate.py diff --git a/README.md b/README.md new file mode 100644 index 0000000..f71ea41 --- /dev/null +++ b/README.md @@ -0,0 +1,22 @@ +# Gmirator + +This tool was made so I could write my content only in `gmi` and publish both a +gemini capsule and a website. + +## TODO + +- [X] concatenate header + gmi2html + footer +- [ ] allow some special characters to start a `

` + - like the `*` for emphasis or `~~` for a strikethrough +- [X] write the `gen_gmi` function +- [X] make the line "---\n" of the footer only present in the code, not in the footer + file +- [X] make a config file for things like + - directories where files are supposed to be + - names of certain files + - the remote servers for the sync feature +- [X] also update the "/gemlog/index.gmi" with the list of articles +- [X] write the sync features +- [X] test the `gen gmi` and `gen html` features + - [ ] write tests for it +- [ ] move config file to `~/.config` with placeholder values diff --git a/gmirator/__init__.py b/gmirator/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/gmirator/generate.py b/gmirator/generate.py new file mode 100644 index 0000000..9cc96cc --- /dev/null +++ b/gmirator/generate.py @@ -0,0 +1,171 @@ +import typer +import re +import shutil +from io import TextIOWrapper +from pathlib import Path + +import json + +config_path = "config.json" +with open(config_path) as f: + config = json.load(f) + +app = typer.Typer(help="generates your gmi and html files") + +WORKDIR = Path(config["workdir"]) +DIR_ASSETS = WORKDIR / config["dir_assets"] +DIR_SOURCE = WORKDIR / config["dir_source"] +DIR_HELPER = WORKDIR / config["dir_helper"] +DIR_O_HTML = WORKDIR / config["dir_o_html"] + +@app.command("gmi") +def gen_gmi(): + print("Generating GMI files...") + make_index([DIR_SOURCE / "index.gmi", DIR_SOURCE / "gemlog/index.gmi"]) + make_footer(WORKDIR / "footer.gmi", DIR_SOURCE) + print("Done!") + +def make_index(listing: list[Path]): + print("Populating gemlog index ...") + title, date = "", "" + files = (DIR_SOURCE / "gemlog").rglob(r'*.gmi') + files = [ \ + file for file in files \ + if re.search(r'\d\d\d\d.*\.gmi', file.as_posix()) \ + ] + articles = "" + for file in files[::-1]: + title, date = find_title_date(file) + print(f"\u2713 {date} {title.strip()}") + articles += f"=> /gemlog/{file.stem}.gmi {date} - {title}" + + for path in listing: + with open(path, "r+") as file: + line = file.readline(9) + while line != "## logs\n": + line = file.readline(9) + file.seek(0, 1) + if not path.match(r"gemlog/index.gmi"): + file.write("=> /gemlog/index.gmi all gemlogs here\n") + file.write("\n" + articles) + file.truncate() + +def make_footer(footer_path: Path, directory: Path, footer_str: str ="\n---\n"): + with open(footer_path) as file: + footer = file.read() + + print("Adding footers...") + files = directory.rglob(r'*.gmi') + for file in files: + with open(file, "rb+") as f: + while line:= f.readline(): + if line == "---\n".encode("utf-8"): + f.seek(-5, 1) + break + f.write((footer_str + footer).encode("utf-8")) + f.truncate() + print(f"\u2713 {file}") + print("Footers added !") + +def find_title_date(filename: Path): + title, date = "", filename.stem[:10] + with open(filename) as file: + for line in file: + if line[:2] == "# ": + title = line[2:] + return title, date + +@app.command("html") +def gen_html(): + files = DIR_SOURCE.rglob('*.gmi') + + for file in files: + gmi2html(file.as_posix()) + print("---\nDone processing gmi files\n") + + print("\nCopying static files...\n") + shutil.copytree(DIR_ASSETS, DIR_O_HTML, dirs_exist_ok=True) + +def gmi2html(file: str): + new_file = file.replace(f"{DIR_SOURCE}", f"{DIR_O_HTML}") + new_file = Path(new_file).with_suffix('.html') + new_file.parent.mkdir(parents=True, exist_ok=True) + + print(f"Processing: {new_file}") + + new_file = open(new_file, "w") + with open(file, 'r') as gemlog: + contents, title = process_file(gemlog) + with open(DIR_HELPER / "header-part.html", "r") as header_file: + x = header_file.read() + x = re.sub("PAGETITLE", f"{title}", x) + new_file.write(x) + new_file.write(contents) + with open(DIR_HELPER / "footer-part.html", "r") as footer_file: + new_file.write(footer_file.read()) + new_file.close() + +def process_file(ifile: TextIOWrapper): + in_preformated = False + in_list = False + contents = "" + title = "" + for line in ifile: + if line == "---\n": + break + elif line[:2] == "# ": + title = line[2:-1] + + newline = line + + if in_preformated: + newline, n = re.subn(r'^```.*', "", newline) + in_preformated = False if n else True + contents += newline.lstrip() + continue + else: + newline, n = re.subn(r'^```.*', "

", newline)
+            in_preformated = True if n else False
+
+        if re.match(r'^[a-zA-Z]', newline):
+            newline = "

" + newline.strip() + "

\n" + newline, in_list = process_list(newline, in_list) + newline = re.sub(r'^=> (?P[^ ]*) ?(?P.*)\n', repl_url, newline) + newline = re.sub(r'^(#+) (.*)', repl_heading, newline) + newline = process_inline(newline) + contents += newline + return contents, title + +def process_list(line: str, in_list: bool): + if in_list: + if line[0] == "*": + line = re.sub(r'^\* (.*)', r'
  • \1
  • ', line) + else: + in_list = False + line = "\n\n" + elif line[0] == "*": + line = "
      \n" + re.sub(r'^\* (.*)', r'
    • \1
    • ', line) + in_list = True + return line, in_list + +def process_inline(line: str): + repls = { + r'\*(.*?)\*': r'\1', + r'~~(.*?)~~': r'\1' + } + for pattern, repl in repls.items(): + line = re.sub(pattern, repl, line) + return line + +def repl_url(matchobj: re.Match): + url, text = matchobj.groupdict().values() + if not re.findall(r'=> (http|gemini)', url): + url = re.sub(r'gmi$', r'html', url) + return f'=> {text if text else url}
      \n' + +def repl_heading(matchobj: re.Match): + x = len(matchobj.group(1)) + return f"{matchobj.string.strip()}" + +if __name__ == "__main__": + app() diff --git a/gmirator/main.py b/gmirator/main.py new file mode 100644 index 0000000..5ea1dcf --- /dev/null +++ b/gmirator/main.py @@ -0,0 +1,14 @@ +import typer +import generate +import synchronise + + +app = typer.Typer( + add_completion=False, + help="gmirator helps you publish in both gmi and html" +) +app.add_typer(generate.app, name='gen') +app.add_typer(synchronise.app, name='sync') + +if __name__ == "__main__": + app() diff --git a/gmirator/synchronise.py b/gmirator/synchronise.py new file mode 100644 index 0000000..31b0603 --- /dev/null +++ b/gmirator/synchronise.py @@ -0,0 +1,33 @@ +import typer +import subprocess +from pathlib import Path +import shlex +import json + +config_path = "config.json" +with open(config_path) as f: + config = json.load(f) +WORKDIR = Path(config["workdir"]) +RPORT = config["port"] + +app = typer.Typer(help="syncs your files with configured remote") + +@app.command("gmi") +def sync_gmi(): + print("Synchronising GMI content with remote...") + LGMI = WORKDIR / "content" + RGMI = config["rem_gmi"] + subprocess.call( + shlex.split(f'rsync -aP -e "ssh -p {RPORT}" {LGMI} {RGMI}') + ) + print("Done!") + +@app.command("html") +def sync_html(): + print("Synchronising HTML content with remote...") + LHTML = WORKDIR / config["dir_o_html"] + RHTML = config["rem_html"] + subprocess.call( + shlex.split(f'rsync -aP -e "ssh -p {RPORT}" {LHTML} {RHTML}') + ) + print("Done!") diff --git a/gmirator/test/__init__.py b/gmirator/test/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/gmirator/test/test_generate.py b/gmirator/test/test_generate.py new file mode 100644 index 0000000..37fbf11 --- /dev/null +++ b/gmirator/test/test_generate.py @@ -0,0 +1,5 @@ +from gmirator.generate import process_inline + + +def test_process_inline(): + assert process_inline("*Hello* ~~world~~!") == "Hello world!"