about summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--src/wikme.scm6
-rw-r--r--wikme-0.scm105
-rw-r--r--wikme.py106
-rw-r--r--wikme.scm45
4 files changed, 243 insertions, 19 deletions
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 @@
37 37
38(define (directory->wiki directory 38(define (directory->wiki directory
39 #!key 39 #!key
40 base-url
41 destination-directory
42 page-template
40 (extension "md") 43 (extension "md")
41 (base-url "https://www.example.com")
42 (destination-directory (make-pathname directory "out"))
43 (page-template (make-pathname directory "template.html"))
44 (file-transformers (list indexify)) 44 (file-transformers (list indexify))
45 (transformers (list cmark->html wikify-links))) 45 (transformers (list cmark->html wikify-links)))
46 ;;; Build a <wiki> out of the markdown files in DIRECTORY. 46 ;;; Build a <wiki> 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 @@
1;;; Wikme --- build a static wiki from a folder full of markdown
2;; -*- geiser-scheme-implementation: chicken -*-
3
4;; Copyright (C) C. Duckworth <acdw@acdw.net> under the GPL-MD license.
5;; See COPYING for details.
6;; Written with help from (and thanks to!) S. Dunlap
7
8(import
9 (chicken irregex) ; Regex engine
10 (chicken file posix) ; File access
11 (chicken port) ; Input/Output ports
12 (chicken process) ; Processes and pipes
13 (ersatz) ; Jinja-compatible templating
14 (filepath) ; File paths
15 (lowdown) ; Markdown parser
16 (srfi-19) ; Time library
17 (srfi-19-io) ; Time input/output
18 (srfi-152) ; String library
19 (utf8)) ; UTF8 support
20
21
22;;; Strings
23
24(define (string-capitalize str)
25 "Capitalize the first word in STR, and ensure the rest of it is lowercase."
26 ;; Stolen and adapted from MIT/GNU Scheme
27 (let* ((end (string-length str))
28 (str* (make-string end)))
29 (do ((i 0 (+ i 1)))
30 ((= i end))
31 (string-set! str* i ((if (= i 0) char-upcase char-downcase)
32 (string-ref str i))))
33 str*))
34
35(define (slugify str)
36 "Convert STR to a 'slug', that is, another string suitable for linking.
37This function will return the input string, in sentence case, and with all
38punctuation and spaces converted to a hypen."
39 (string-capitalize
40 (string-trim-both (irregex-replace/all '(+ (~ alnum)) str "-")
41 (lambda (c)
42 (char=? c #\-)))))
43
44(define (unslugify slug)
45 "Convert a SLUG back into a normal string as best as possible.
46Because information is lost in slugification, it's impossible to be sure that
47the result of this procedure is totally accurate. That is, slugification is not
48round-trippable."
49 (irregex-replace/all '("-") slug " "))
50
51
52;;; Pages
53
54(define-record-type <page>
55 (make-page title content source last-edited)
56 page?
57 (title page-title page-title-set!)
58 (content page-content page-content-set!)
59 (source page-source page-source-set!)
60 (last-edited page-last-edited page-last-edited-set!))
61
62(define (read-page file)
63 "Read a <page> record from FILE."
64 (let* ((src (with-input-from-file file read-string))
65 (sxml (call-with-input-string src markdown->sxml)))
66 (title (or (extract-title sxml)
67 (unslugify (filepath:take-base-name file)))))
68 (make-page title
69 sxml
70 src
71 (get-last-mtime file)))
72
73(define (get-last-mtime file)
74 "Figure out FILE's mtime.
75First, try running a git log command. If that doesn't work, use the file
76system."
77 (seconds->time
78 (or (string->number
79 (string-trim-both
80 (with-input-from-pipe
81 (string-append "git -C " _ "log -1 --format=%ct --date=unix " file)
82 read-string)))
83 (file-modification-time file))))
84
85
86;;; Templates
87
88(define (render-template template page
89 #!key
90 (last-updated-format "~4")
91 (escape-html #f)
92 )
93 "Render PAGE using TEMPLATE.
94TEMPLATE is a jinja2-compatible template file and PAGE is a <page> record type.
95TEMPLATE will be passed the following variables:
96- title: the page title
97- body: the page body as HTML, escaped depending on ESCAPE-HTML (default #f).
98- last_updated: the time the page was updated in LAST-UPDATED-FORMAT
99 (default ISO-8601 year-month-day-hour-minute-second-timezone)"
100 (from-file template
101 #:env (template-std-env #:autoescape escape-html)
102 #:models `((title . ,(Tstr (page-title page)))
103 (body . ,(Tstr (page-content page)))
104 (last_updated . ,(Tstr (format-date #f last-updated-format
105 (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 @@
1import os
2import re
3import argparse
4import markdown
5import datetime
6import subprocess
7import shutil
8from typing import Optional
9
10def kebab_case(s: str) -> str:
11 return re.sub(r"[ _]", "-", s).lower()
12
13def get_title(filename: str, content: Optional[str] = None) -> str:
14 if content:
15 # Check for a top-level header in the content
16 top_level_header = re.search(r"^#\s(.+)$", content, re.MULTILINE)
17 if top_level_header:
18 return top_level_header.group(1).strip()
19
20 # Extract the inferred title from the filename
21 title = filename.replace(".md", "").replace("_", " ")
22 return title.capitalize()
23
24def parse_wikilinks(content: str) -> str:
25 # Convert wikilinks with tildes and custom titles
26 content = re.sub(r'\[\[(~[^|\]]+?)\|([^|\]]+?)\]\]', r'<a href="/\1">\2</a>', content)
27
28 # Convert wikilinks with tildes and without custom titles
29 content = re.sub(r'\[\[(~[^|\]]+?)\]\]', r'<a href="/\1">\1</a>', content)
30
31 # Convert regular wikilinks with custom titles
32 content = re.sub(r'\[\[([^~|\]]+?)\|([^~|\]]+?)\]\]', lambda match: f'<a href="./{kebab_case(match.group(1))}.html">{match.group(2)}</a>', content)
33
34 # Convert regular wikilinks without custom titles
35 content = re.sub(r'\[\[([^~|\]]+?)\]\]', lambda match: f'<a href="./{kebab_case(match.group(1))}.html">{match.group(1)}</a>', content)
36
37 return content
38
39def render_template(template: str, title: str, content: str, last_edited: str) -> str:
40 # Insert title, content, and last_edited into the template
41 rendered = template.replace("{{ title }}", title)
42 rendered = rendered.replace("{{ content }}", content)
43 rendered = rendered.replace("{{ last_edited }}", last_edited)
44 return rendered
45
46def get_last_edited(path: str) -> str:
47 try:
48 # Attempt to get the last Git commit date of the file
49 last_edited = subprocess.check_output(
50 ["git", "log", "-1", "--format=%cd", "--date=local", path])
51 return last_edited.decode("utf-8").strip()
52 except Exception:
53 # Fallback to the file's modified timestamp
54 return str(datetime.datetime.fromtimestamp(os.path.getmtime(path)))
55
56def main(input_folder: str, output_folder: str, template_file: str):
57 # Load the template
58 with open(template_file, "r") as template_f:
59 template = template_f.read()
60
61 # Go through each markdown file
62 for root, dirs, files in os.walk(input_folder):
63 for file in files:
64 if file.endswith(".md"):
65 # Process the markdown file
66 input_file = os.path.join(root, file)
67 output_subfolder = os.path.join(
68 output_folder, os.path.relpath(root, input_folder))
69 output_file = os.path.join(
70 output_subfolder, f"{kebab_case(file.replace('.md', ''))}.html")
71
72 # Read the source file
73 with open(input_file, "r") as source:
74 markdown_content = source.read()
75 html_content = markdown.markdown(
76 parse_wikilinks(markdown_content), extensions=['codehilite']
77 )
78
79 # Create the output folder if needed
80 if not os.path.exists(output_subfolder):
81 os.makedirs(output_subfolder)
82
83 # Render the result
84 title = get_title(file, markdown_content)
85 last_edited = get_last_edited(input_file)
86 rendered_content = render_template(template, title, html_content, last_edited)
87
88 # Save the rendered HTML file
89 with open(output_file, "w") as output_f:
90 output_f.write(rendered_content)
91
92def cmd():
93 parser = argparse.ArgumentParser(
94 description="Convert a folder of Markdown files into a simple wiki-style website.")
95 parser.add_argument('--input', dest='input_folder',
96 required=True, help='input folder containing Markdown files')
97 parser.add_argument('--output', dest='output_folder',
98 required=True, help='output folder for generated HTML files')
99 parser.add_argument('--template', dest='template_file',
100 required=True, help='HTML template for the generated files')
101 args = parser.parse_args()
102
103 main(args.input_folder, args.output_folder, args.template_file)
104
105if __name__ == "__main__":
106 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 @@
1;;; wikme.scm --- build a wiki from a folder of markdown --- executable 1;;; wikme.scm --- build a wiki from a folder of markdown --- executable
2 2
3(import (args) 3(import (args)
4 (chicken pathname)
4 (chicken process-context) 5 (chicken process-context)
5 (chicken port)) 6 (chicken port))
6 7
@@ -8,32 +9,44 @@
8 9
9 10
10 11
11 ;; (make-wiki base-url ; base URL for links 12(define +opts+
12 ;; origin-dir ; origin directory 13 (list (args:make-option
13 ;; destination-dir ; destination directory 14 (u base-url) (optional: "URL")
14 ;; page-template ; template for pages 15 "Base URL for the generated Wiki.")
15 ;; file-transformers ; list of filename transformers 16 (args:make-option
16 ;; transformers ; list of source transformer functions 17 (s source) (optional: "DIRECTORY")
17 ;; pages ; list of <page>s 18 "Directory containing source files (default: PWD).")
18 ;; ) 19 (args:make-option
19 20 (o out) (optional: "DIRECTORY")
20 21 "Directory in which to place rendered files (default: PWD/out).")
21(define options 22 (args:make-option
22 (list (args:make-option ))) 23 (t template) (optional: "FILE")
24 "Template file for wiki pages (default: PWD/template.html).")))
23 25
24 26
25 27
26(define (usage) 28(define (usage)
27 (with-output-to-port (current-error-port) 29 (with-output-to-port (current-error-port)
28 (lambda () 30 (lambda ()
29 (print "Usage: " (car (argv)) " [options...] [directory]") 31 (print "Usage: " (car (argv)) " [options...]")
30 (newline) 32 (newline)
31 (print (args:usage options)))) 33 (print (args:usage +opts+))))
32 (exit 1)) 34 (exit 1))
33 35
34(define (main args) 36(define (main args)
35 (receive (options operands) 37 (receive (options operands)
36 (args:parse args options) 38 (args:parse args +opts+)
37 #f)) 39 (render-wiki
40 (directory->wiki
41 (or (alist-ref 'source options)
42 (current-directory))
43 #:base-url (or (alist-ref 'base-url options)
44 "https://www.example.com")
45 #:destination-directory (or (alist-ref 'out options)
46 (make-pathname
47 (current-directory) "out"))
48 #:page-template (or (alist-ref 'template options)
49 (make-pathname
50 (current-directory "template.html")))))))
38 51
39(main (command-line-arguments)) 52(main (command-line-arguments))