|
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185 |
- #!/usr/bin/env python3
-
- import codecs
- import fnmatch
- import locale
- import os
- import socketserver
- from dataclasses import dataclass
- from datetime import date, datetime
- from html import escape
- from http.server import SimpleHTTPRequestHandler
- from operator import attrgetter
- from pathlib import Path
- from time import perf_counter
-
- import markdown
- from jinja2 import Environment as Env
- from jinja2 import FileSystemLoader
- from minicli import cli, run, wrap
-
- # Useful for dates rendering within Jinja2.
- locale.setlocale(locale.LC_ALL, "fr_FR.UTF-8")
-
- HERE = Path(".")
- DAVID = HERE / "david"
- DOMAIN = "https://larlet.fr"
- # Hardcoding publication at 12 in Paris timezone.
- NORMALIZED_STRFTIME = "%Y-%m-%dT12:00:00+01:00"
-
- environment = Env(loader=FileSystemLoader(str(DAVID / "templates")))
-
-
- def neighborhood(iterable, first=None, last=None):
- """
- Yield the (previous, current, next) items given an iterable.
-
- You can specify a `first` and/or `last` item for bounds.
- """
- iterator = iter(iterable)
- previous = first
- current = next(iterator) # Throws StopIteration if empty.
- for next_ in iterator:
- yield (previous, current, next_)
- previous = current
- current = next_
- yield (previous, current, last)
-
-
- def parse_markdown(file_path):
- """Extract title, (HTML) content and metadata from a markdown file."""
- parser = markdown.Markdown(extensions=["meta"])
- with codecs.open(file_path, "r") as source:
- content = parser.convert(source.read())
- metadata = parser.Meta if hasattr(parser, "Meta") else None
- title = metadata["title"][0] if metadata is not None else ""
- return title, content, metadata
-
-
- def each_markdown_from(source_dir, file_name="index.md"):
- """Walk across the `source_dir` and return the md file paths."""
- for root, dirnames, filenames in os.walk(source_dir):
- for filename in fnmatch.filter(filenames, file_name):
- yield os.path.join(root, filename)
-
-
- @dataclass
- class Note:
- title: str
- content: str
- file_path: str
-
- def __post_init__(self):
- suffix = len("/index.md")
- prefix = len("YYYY/MM/DD") + suffix
- date_str = self.file_path[-prefix:-suffix]
- self.url = f"/david/stream/{date_str}/"
- self.full_url = f"{DOMAIN}{self.url}"
- self.date = datetime.strptime(date_str, "%Y/%m/%d").date()
- self.normalized_date = self.date.strftime(NORMALIZED_STRFTIME)
- self.escaped_title = escape(self.title)
- self.escaped_content = escape(
- self.content.replace('href="/', f'href="{DOMAIN}/').replace(
- 'src="/', f'src="{DOMAIN}/'
- )
- )
- self.extract = self.content.split("</p>", 1)[0] + "</p>"
-
- @property
- def is_draft(self):
- return self.date > date.today()
-
- @staticmethod
- def all(source, only_published=True):
- """Retrieve all (published) notes sorted by date desc."""
- note_list = []
- for file_path in each_markdown_from(source):
- title, content, _ = parse_markdown(file_path)
- note = Note(title, content, file_path)
- if only_published and note.date > date.today():
- continue
- note_list.append(note)
- return sorted(note_list, key=attrgetter("date"), reverse=True)
-
-
- @cli
- def note(when=None):
- """Create a new note and open it in iA Writer.
-
- :when: Optional date in ISO format (YYYY-MM-DD)
- """
- when = datetime.strptime(when, "%Y-%m-%d") if when else date.today()
- note_path = DAVID / "stream" / str(when.year) / str(when.month) / str(when.day)
- os.makedirs(note_path)
- filename = note_path / "index.md"
- open(filename, "w+").write("title: ")
- os.popen(f'open -a "iA Writer" "{filename}"')
-
-
- @cli
- def stream():
- """Generate articles and archives for the stream."""
- template_article = environment.get_template("stream_2019_article.html")
- template_archives = environment.get_template("stream_2019_archives.html")
- # Default when you reach the last item.
- notes_2018 = Note(
- title="Anciennes notes (2018)",
- content="",
- file_path="/david/stream/2018/12/31/index.md",
- )
- note_base = DAVID / "stream" / "2019"
- published = Note.all(source=note_base)
- unpublished = Note.all(source=note_base, only_published=False)
- for previous, note, next_ in neighborhood(unpublished, last=notes_2018):
- if note.is_draft:
- print(f"Soon: http://larlet.test:8001/{note.url} ({note.title})")
- # Detect if there is code for syntax highlighting + monospaced font.
- has_code = "<code>" in note.content
- # Do not link to unpublished notes.
- previous = previous and not previous.is_draft and previous or None
- page_article = template_article.render(
- note=note,
- next=previous,
- prev=next_,
- has_code=has_code,
- note_list=published,
- )
- open(
- note_base / f"{note.date.month:02}" / f"{note.date.day:02}" / "index.html",
- "w",
- ).write(page_article)
-
- page_archive = template_archives.render(note_list=published)
- open(note_base / "index.html", "w").write(page_archive)
- print(f"Done: http://larlet.test:8001/{note_base}/")
-
-
- @cli
- def feed():
- """Generate a feed from 15 last published Notes in stream."""
- template = environment.get_template("feed.xml")
- content = template.render(
- note_list=Note.all(source=DAVID / "stream" / "2019")[:15],
- current_dt=datetime.now().strftime(NORMALIZED_STRFTIME),
- BASE_URL=f"{DOMAIN}/david/",
- )
- open(DAVID / "log" / "index.xml", "w").write(content)
-
-
- @cli
- def serve():
- httpd = socketserver.TCPServer(("larlet.test", 8001), SimpleHTTPRequestHandler)
- print("Serving at http://larlet.test:8001/david/")
- httpd.serve_forever()
-
-
- @wrap
- def perf_wrapper():
- start = perf_counter()
- yield
- elapsed = perf_counter() - start
- print(f"Done in {elapsed:.5f} seconds.")
-
-
- if __name__ == "__main__":
- run()
|