diff options
-rw-r--r-- | src/wikme.scm | 6 | ||||
-rw-r--r-- | wikme-0.scm | 105 | ||||
-rw-r--r-- | wikme.py | 106 | ||||
-rw-r--r-- | wikme.scm | 45 |
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. | ||
37 | This function will return the input string, in sentence case, and with all | ||
38 | punctuation 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. | ||
46 | Because information is lost in slugification, it's impossible to be sure that | ||
47 | the result of this procedure is totally accurate. That is, slugification is not | ||
48 | round-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. | ||
75 | First, try running a git log command. If that doesn't work, use the file | ||
76 | system." | ||
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. | ||
94 | TEMPLATE is a jinja2-compatible template file and PAGE is a <page> record type. | ||
95 | TEMPLATE 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 @@ | |||
1 | import os | ||
2 | import re | ||
3 | import argparse | ||
4 | import markdown | ||
5 | import datetime | ||
6 | import subprocess | ||
7 | import shutil | ||
8 | from typing import Optional | ||
9 | |||
10 | def kebab_case(s: str) -> str: | ||
11 | return re.sub(r"[ _]", "-", s).lower() | ||
12 | |||
13 | def 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 | |||
24 | def 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 | |||
39 | def 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 | |||
46 | def 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 | |||
56 | def 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 | |||
92 | def 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 | |||
105 | if __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)) |