From 920b1d5df49d0bf16084150ac0994807f9c66447 Mon Sep 17 00:00:00 2001 From: Case Duckworth Date: Wed, 29 Mar 2023 23:33:04 -0500 Subject: Ready for testing --- src/wikme.scm | 6 ++-- wikme-0.scm | 105 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ wikme.py | 106 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ wikme.scm | 45 ++++++++++++++++--------- 4 files changed, 243 insertions(+), 19 deletions(-) create mode 100644 wikme-0.scm create mode 100644 wikme.py diff --git a/src/wikme.scm b/src/wikme.scm index 2d476ca..752aa8b 100644 --- a/src/wikme.scm +++ b/src/wikme.scm @@ -37,10 +37,10 @@ (define (directory->wiki directory #!key + base-url + destination-directory + page-template (extension "md") - (base-url "https://www.example.com") - (destination-directory (make-pathname directory "out")) - (page-template (make-pathname directory "template.html")) (file-transformers (list indexify)) (transformers (list cmark->html wikify-links))) ;;; Build a out of the markdown files in DIRECTORY. diff --git a/wikme-0.scm b/wikme-0.scm new file mode 100644 index 0000000..ea99125 --- /dev/null +++ b/wikme-0.scm @@ -0,0 +1,105 @@ +;;; Wikme --- build a static wiki from a folder full of markdown +;; -*- geiser-scheme-implementation: chicken -*- + +;; Copyright (C) C. Duckworth under the GPL-MD license. +;; See COPYING for details. +;; Written with help from (and thanks to!) S. Dunlap + +(import + (chicken irregex) ; Regex engine + (chicken file posix) ; File access + (chicken port) ; Input/Output ports + (chicken process) ; Processes and pipes + (ersatz) ; Jinja-compatible templating + (filepath) ; File paths + (lowdown) ; Markdown parser + (srfi-19) ; Time library + (srfi-19-io) ; Time input/output + (srfi-152) ; String library + (utf8)) ; UTF8 support + + +;;; Strings + +(define (string-capitalize str) + "Capitalize the first word in STR, and ensure the rest of it is lowercase." + ;; Stolen and adapted from MIT/GNU Scheme + (let* ((end (string-length str)) + (str* (make-string end))) + (do ((i 0 (+ i 1))) + ((= i end)) + (string-set! str* i ((if (= i 0) char-upcase char-downcase) + (string-ref str i)))) + str*)) + +(define (slugify str) + "Convert STR to a 'slug', that is, another string suitable for linking. +This function will return the input string, in sentence case, and with all +punctuation and spaces converted to a hypen." + (string-capitalize + (string-trim-both (irregex-replace/all '(+ (~ alnum)) str "-") + (lambda (c) + (char=? c #\-))))) + +(define (unslugify slug) + "Convert a SLUG back into a normal string as best as possible. +Because information is lost in slugification, it's impossible to be sure that +the result of this procedure is totally accurate. That is, slugification is not +round-trippable." + (irregex-replace/all '("-") slug " ")) + + +;;; Pages + +(define-record-type + (make-page title content source last-edited) + page? + (title page-title page-title-set!) + (content page-content page-content-set!) + (source page-source page-source-set!) + (last-edited page-last-edited page-last-edited-set!)) + +(define (read-page file) + "Read a record from FILE." + (let* ((src (with-input-from-file file read-string)) + (sxml (call-with-input-string src markdown->sxml))) + (title (or (extract-title sxml) + (unslugify (filepath:take-base-name file))))) + (make-page title + sxml + src + (get-last-mtime file))) + +(define (get-last-mtime file) + "Figure out FILE's mtime. +First, try running a git log command. If that doesn't work, use the file +system." + (seconds->time + (or (string->number + (string-trim-both + (with-input-from-pipe + (string-append "git -C " _ "log -1 --format=%ct --date=unix " file) + read-string))) + (file-modification-time file)))) + + +;;; Templates + +(define (render-template template page + #!key + (last-updated-format "~4") + (escape-html #f) + ) + "Render PAGE using TEMPLATE. +TEMPLATE is a jinja2-compatible template file and PAGE is a record type. +TEMPLATE will be passed the following variables: +- title: the page title +- body: the page body as HTML, escaped depending on ESCAPE-HTML (default #f). +- last_updated: the time the page was updated in LAST-UPDATED-FORMAT + (default ISO-8601 year-month-day-hour-minute-second-timezone)" + (from-file template + #:env (template-std-env #:autoescape escape-html) + #:models `((title . ,(Tstr (page-title page))) + (body . ,(Tstr (page-content page))) + (last_updated . ,(Tstr (format-date #f last-updated-format + (page-last-edited page))))))) diff --git a/wikme.py b/wikme.py new file mode 100644 index 0000000..2df5a03 --- /dev/null +++ b/wikme.py @@ -0,0 +1,106 @@ +import os +import re +import argparse +import markdown +import datetime +import subprocess +import shutil +from typing import Optional + +def kebab_case(s: str) -> str: + return re.sub(r"[ _]", "-", s).lower() + +def get_title(filename: str, content: Optional[str] = None) -> str: + if content: + # Check for a top-level header in the content + top_level_header = re.search(r"^#\s(.+)$", content, re.MULTILINE) + if top_level_header: + return top_level_header.group(1).strip() + + # Extract the inferred title from the filename + title = filename.replace(".md", "").replace("_", " ") + return title.capitalize() + +def parse_wikilinks(content: str) -> str: + # Convert wikilinks with tildes and custom titles + content = re.sub(r'\[\[(~[^|\]]+?)\|([^|\]]+?)\]\]', r'\2', content) + + # Convert wikilinks with tildes and without custom titles + content = re.sub(r'\[\[(~[^|\]]+?)\]\]', r'\1', content) + + # Convert regular wikilinks with custom titles + content = re.sub(r'\[\[([^~|\]]+?)\|([^~|\]]+?)\]\]', lambda match: f'{match.group(2)}', content) + + # Convert regular wikilinks without custom titles + content = re.sub(r'\[\[([^~|\]]+?)\]\]', lambda match: f'{match.group(1)}', content) + + return content + +def render_template(template: str, title: str, content: str, last_edited: str) -> str: + # Insert title, content, and last_edited into the template + rendered = template.replace("{{ title }}", title) + rendered = rendered.replace("{{ content }}", content) + rendered = rendered.replace("{{ last_edited }}", last_edited) + return rendered + +def get_last_edited(path: str) -> str: + try: + # Attempt to get the last Git commit date of the file + last_edited = subprocess.check_output( + ["git", "log", "-1", "--format=%cd", "--date=local", path]) + return last_edited.decode("utf-8").strip() + except Exception: + # Fallback to the file's modified timestamp + return str(datetime.datetime.fromtimestamp(os.path.getmtime(path))) + +def main(input_folder: str, output_folder: str, template_file: str): + # Load the template + with open(template_file, "r") as template_f: + template = template_f.read() + + # Go through each markdown file + for root, dirs, files in os.walk(input_folder): + for file in files: + if file.endswith(".md"): + # Process the markdown file + input_file = os.path.join(root, file) + output_subfolder = os.path.join( + output_folder, os.path.relpath(root, input_folder)) + output_file = os.path.join( + output_subfolder, f"{kebab_case(file.replace('.md', ''))}.html") + + # Read the source file + with open(input_file, "r") as source: + markdown_content = source.read() + html_content = markdown.markdown( + parse_wikilinks(markdown_content), extensions=['codehilite'] + ) + + # Create the output folder if needed + if not os.path.exists(output_subfolder): + os.makedirs(output_subfolder) + + # Render the result + title = get_title(file, markdown_content) + last_edited = get_last_edited(input_file) + rendered_content = render_template(template, title, html_content, last_edited) + + # Save the rendered HTML file + with open(output_file, "w") as output_f: + output_f.write(rendered_content) + +def cmd(): + parser = argparse.ArgumentParser( + description="Convert a folder of Markdown files into a simple wiki-style website.") + parser.add_argument('--input', dest='input_folder', + required=True, help='input folder containing Markdown files') + parser.add_argument('--output', dest='output_folder', + required=True, help='output folder for generated HTML files') + parser.add_argument('--template', dest='template_file', + required=True, help='HTML template for the generated files') + args = parser.parse_args() + + main(args.input_folder, args.output_folder, args.template_file) + +if __name__ == "__main__": + cmd() \ No newline at end of file diff --git a/wikme.scm b/wikme.scm index 7f992a3..74d672f 100644 --- a/wikme.scm +++ b/wikme.scm @@ -1,6 +1,7 @@ ;;; wikme.scm --- build a wiki from a folder of markdown --- executable (import (args) + (chicken pathname) (chicken process-context) (chicken port)) @@ -8,32 +9,44 @@ - ;; (make-wiki base-url ; base URL for links - ;; origin-dir ; origin directory - ;; destination-dir ; destination directory - ;; page-template ; template for pages - ;; file-transformers ; list of filename transformers - ;; transformers ; list of source transformer functions - ;; pages ; list of s - ;; ) - - -(define options - (list (args:make-option ))) +(define +opts+ + (list (args:make-option + (u base-url) (optional: "URL") + "Base URL for the generated Wiki.") + (args:make-option + (s source) (optional: "DIRECTORY") + "Directory containing source files (default: PWD).") + (args:make-option + (o out) (optional: "DIRECTORY") + "Directory in which to place rendered files (default: PWD/out).") + (args:make-option + (t template) (optional: "FILE") + "Template file for wiki pages (default: PWD/template.html)."))) (define (usage) (with-output-to-port (current-error-port) (lambda () - (print "Usage: " (car (argv)) " [options...] [directory]") + (print "Usage: " (car (argv)) " [options...]") (newline) - (print (args:usage options)))) + (print (args:usage +opts+)))) (exit 1)) (define (main args) (receive (options operands) - (args:parse args options) - #f)) + (args:parse args +opts+) + (render-wiki + (directory->wiki + (or (alist-ref 'source options) + (current-directory)) + #:base-url (or (alist-ref 'base-url options) + "https://www.example.com") + #:destination-directory (or (alist-ref 'out options) + (make-pathname + (current-directory) "out")) + #:page-template (or (alist-ref 'template options) + (make-pathname + (current-directory "template.html"))))))) (main (command-line-arguments)) -- cgit 1.4.1-21-gabe81