allow setting fields other than 'text' in tiddlers

The interface isn't the cleanest (it'd probably be better to map from
tiddlers to a set of fields, rather than having a different set of
mappings for each field), but it will do for now. Needing more
than a few of these is a Zettelkasten-design smell anyway.
This commit is contained in:
Soren I. Bjornstad 2021-09-20 12:12:18 -05:00
parent 5065efd37e
commit 197ec6c8b2
1 changed files with 91 additions and 26 deletions

View File

@ -10,6 +10,7 @@ time. This allows the configuration file to be read at any time to retrieve
information about the defined products without actually running any build steps.
"""
from collections.abc import Mapping
from contextlib import contextmanager
import functools
import json
@ -19,7 +20,7 @@ import re
import shutil
import subprocess
import tempfile
from typing import Callable, Dict, List, Set, Sequence, Tuple
from typing import Callable, Dict, List, Optional, Set, Sequence, Tuple
from tzk import git
from tzk import tw
@ -419,10 +420,83 @@ def replace_private_people(initialer: Callable[[str], str] = None) -> None:
f.writelines(lines)
@tzk_builder
def set_tiddler_values(mappings: Dict[str, str]) -> None:
def _set_fields(mappings: Dict[str, str],
editing_func: Callable[[Path, List[str], str], None]) -> None:
"""
Set the ``text`` field of selected config or other tiddlers to arbitrary new values.
Read the text of a .tid file into memory and call an editing_func on it
in order to modify some of its fields.
:param mappings: Mapping of tiddler filenames to new values of some field
(this function is field-agnostic; the editing_func should be
aware of what field is to be edited).
:param editing_func: A function which will make the changes.
The editing_func is called once for each item in the mapping
and receives three arguments:
:param tiddler_path: The full path to the tiddler on the filesystem.
The editing_func should write changes back to this file,
if any are appropriate.
:param tiddler_lines: A sequence of strings, each one line of the original file.
:param new_text: The new field value specified for this tiddler in the provided
mapping dict of tiddler name/new value.
"""
for tiddler, new_text in mappings.items():
tiddler_path = (Path(build_state['public_wiki_folder']) / "tiddlers" / tiddler)
try:
with tiddler_path.open("r") as f:
tiddler_lines = f.readlines()
except FileNotFoundError:
stop(f"File {tiddler_path} not found. "
f"(Did you forget to end the name with '.tid'?)")
editing_func(tiddler_path, tiddler_lines, new_text)
def _set_text_field(mappings: Dict[str, str]) -> None:
"Set the 'text' field to a new value in each pair of tiddler name/value."
def editor(tiddler_path: Path, tiddler_lines: Sequence[str], new_text: str) -> None:
if not str(tiddler_path).endswith('.tid'):
# will be a separate meta file, so the whole thing is the text field
first_blank_line_index = 0
else:
first_blank_line_index = next(idx
for idx, value in enumerate(tiddler_lines)
if not value.strip())
with tiddler_path.open("w") as f:
f.writelines(tiddler_lines[0:first_blank_line_index+1])
f.write(new_text)
_set_fields(mappings, editor)
def _set_nontext_field(field: str, mappings: Dict[str, str]) -> None:
"Set the specified /field/ to a new value in each pair of tiddler name/value."
def editor(tiddler_path: Path, tiddler_lines: Sequence[str], new_text: str) -> None:
# First check if the field exists and is currently not set to new_text...
line_to_change = -1
for idx, line in enumerate(tiddler_lines):
if m := re.match(f"^{field}: (?P<value>.*)", line):
if m.group('value') != new_text:
line_to_change = idx
break
# ...and if so, write the file again with the change in place.
if line_to_change != -1:
with tiddler_path.open("w") as f:
for idx, line in enumerate(tiddler_lines):
if idx == line_to_change:
f.write(f"{field}: {new_text}\n")
else:
f.write(line)
_set_fields(mappings, editor)
@tzk_builder
def set_tiddler_values(text: Optional[Dict[str, str]] = None,
**kwargs: Dict[str, str]) -> None:
"""
Set fields of selected config or other tiddlers to arbitrary new values.
This can be used to make customizations that can't easily be done with feature
flags or other wikitext solutions within the wiki -- for instance, changing the
@ -436,31 +510,22 @@ def set_tiddler_values(mappings: Dict[str, str]) -> None:
'$__config_sib_CurrentEditionPublicity.tid': 'public',
})
:param mappings: A dictionary whose keys are tiddler filenames
and whose values are the values to be inserted
in those tiddlers' ``text`` fields.
Any number of arguments can be provided. The name of each argument is the name of
the field to edit. One positional argument may be used with no name; this argument
is assumed to be mappings for replacing the 'text' field.
"""
assert 'public_wiki_folder' in build_state
for field_name, field_mapping in kwargs.items():
if not isinstance(field_mapping, Mapping):
raise AssertionError(
"Arguments to set_tiddler_values must be dictionaries "
"mapping tiddler names to values.")
for tiddler, new_text in mappings.items():
tiddler_path = (Path(build_state['public_wiki_folder']) / "tiddlers" / tiddler)
try:
with tiddler_path.open("r") as f:
tiddler_lines = f.readlines()
except FileNotFoundError:
stop(f"File {tiddler_path} not found. "
f"(Did you forget to end the name with '.tid'?)")
if not str(tiddler_path).endswith('.tid'):
# will be a separate meta file, so the whole thing is the text field
first_blank_line_index = 0
else:
first_blank_line_index = next(idx
for idx, value in enumerate(tiddler_lines)
if not value.strip())
with tiddler_path.open("w") as f:
f.writelines(tiddler_lines[0:first_blank_line_index+1])
f.write(new_text)
if text is not None:
_set_text_field(text)
for field, mappings in kwargs.items():
_set_nontext_field(field, mappings)
@tzk_builder