about summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--wikme-0.scm105
-rw-r--r--wikme.py106
3 files changed, 1 insertions, 211 deletions
diff --git a/.gitignore b/.gitignore index 901a834..8270092 100644 --- a/.gitignore +++ b/.gitignore
@@ -1 +1,2 @@
1wikme 1wikme
2ref/
diff --git a/wikme-0.scm b/wikme-0.scm deleted file mode 100644 index ea99125..0000000 --- a/wikme-0.scm +++ /dev/null
@@ -1,105 +0,0 @@
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 deleted file mode 100644 index 2df5a03..0000000 --- a/wikme.py +++ /dev/null
@@ -1,106 +0,0 @@
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