diff options
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | wikme-0.scm | 105 | ||||
-rw-r--r-- | wikme.py | 106 |
3 files changed, 1 insertions, 211 deletions
diff --git a/.gitignore b/.gitignore index 901a834..8270092 100644 --- a/.gitignore +++ b/.gitignore | |||
@@ -1 +1,2 @@ | |||
1 | wikme | 1 | wikme |
2 | ref/ | ||
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. | ||
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 deleted file mode 100644 index 2df5a03..0000000 --- a/wikme.py +++ /dev/null | |||
@@ -1,106 +0,0 @@ | |||
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 | ||