Compare commits
12 Commits
5897a90a99
...
3e9ecd4b70
Author | SHA1 | Date | |
---|---|---|---|
|
3e9ecd4b70 | ||
|
7be018ea3f | ||
|
8fb496bbfe | ||
dcac80c5ac | |||
|
4875f7c1f5 | ||
|
f0bd41f65a | ||
|
d3f6216837 | ||
|
e14709be40 | ||
bdeaac5d03 | |||
|
4acff2731a | ||
|
572c3d0316 | ||
|
4a6e4e7bc0 |
2
LICENSE
2
LICENSE
@ -1,4 +1,4 @@
|
||||
Copyright © 2021 Soren Bjornstad.
|
||||
Copyright © 2021-2022 Soren Bjornstad and the tzk community.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
|
@ -18,9 +18,9 @@ copyright = '2021 Soren Bjornstad'
|
||||
author = 'Soren Bjornstad'
|
||||
|
||||
# The short X.Y version
|
||||
version = "0.1.4"
|
||||
version = "0.2.0"
|
||||
# The full version, including alpha/beta/rc tags
|
||||
release = "0.1.4"
|
||||
release = "0.2.0"
|
||||
|
||||
|
||||
# -- General configuration ---------------------------------------------------
|
||||
|
File diff suppressed because one or more lines are too long
@ -4,6 +4,7 @@
|
||||
# docs
|
||||
sphinx==3.5.4 # newer versions display function names in the wrong color?
|
||||
sphinx_rtd_theme==0.5.2
|
||||
Jinja2<3.1 # https://github.com/readthedocs/readthedocs.org/issues/9038
|
||||
|
||||
# dev tools
|
||||
build
|
||||
|
2
setup.py
2
setup.py
@ -9,7 +9,7 @@ with open("README.md", "r") as fh:
|
||||
|
||||
setuptools.setup(
|
||||
name="tzk",
|
||||
version="0.1.4",
|
||||
version="0.2.0",
|
||||
author="Soren I. Bjornstad",
|
||||
author_email="zettelkasten@sorenbjornstad.com",
|
||||
description="Build tool for TiddlyWiki Zettelkasten",
|
||||
|
@ -13,6 +13,8 @@ from tzk import tw
|
||||
from tzk.util import (BuildError, fail, numerize, require_dependencies, pushd,
|
||||
TZK_VERSION)
|
||||
|
||||
VERSION_INFO = f"tzk version {TZK_VERSION}"
|
||||
|
||||
|
||||
class CliCommand(ABC):
|
||||
"""
|
||||
@ -187,7 +189,7 @@ class VersionCommand(CliCommand):
|
||||
pass
|
||||
|
||||
def execute(self, args: argparse.Namespace) -> None:
|
||||
print(f"tzk version {TZK_VERSION}")
|
||||
print(VERSION_INFO)
|
||||
|
||||
|
||||
class BuildCommand(CliCommand):
|
||||
@ -392,6 +394,7 @@ def launch():
|
||||
# go there before doing anything else.
|
||||
if (not os.path.exists("tzk_config.py")
|
||||
and os.environ.get('TZK_DIRECTORY')
|
||||
and len(sys.argv) > 1
|
||||
and sys.argv[1] != "init"): # we can't init an existing TZK_DIRECTORY
|
||||
try:
|
||||
os.chdir(os.environ['TZK_DIRECTORY'])
|
||||
@ -408,11 +411,17 @@ def launch():
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description=f"TiddlyZettelKasten {TZK_VERSION} CLI\n"
|
||||
f"Copyright (c) 2021 Soren Bjornstad.\n"
|
||||
f"Copyright (c) 2021-2022 Soren Bjornstad and the tzk community.\n"
|
||||
f"MIT license; see https://github.com/sobjornstad/tzk/blob/master/LICENSE for details.",
|
||||
epilog="For full documentation, see https://tzk.readthedocs.io/en/latest/.",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter
|
||||
)
|
||||
parser.add_argument(
|
||||
"--version",
|
||||
action="version",
|
||||
version=VERSION_INFO,
|
||||
help=VersionCommand.help
|
||||
)
|
||||
|
||||
subparsers = parser.add_subparsers()
|
||||
for command in sorted(CliCommand.__subclasses__(), key=lambda i: i.__name__):
|
||||
|
192
tzk/builders.py
192
tzk/builders.py
@ -19,7 +19,7 @@ import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
from typing import Callable, Dict, List, Optional, Set, Sequence, Tuple
|
||||
from typing import Callable, Dict, Generator, List, Optional, Set, Sequence, Tuple
|
||||
|
||||
from tzk import git
|
||||
from tzk import tw
|
||||
@ -359,8 +359,159 @@ def _private_people_replacement_table(
|
||||
}
|
||||
|
||||
|
||||
def _privatize_line(line: str, replacement_table: Dict[str, str],
|
||||
replace_link_text: bool = False) -> Optional[str]:
|
||||
"""
|
||||
Given a line and a table of replacements to make, replace all instances
|
||||
of all private people defined in the replacement table.
|
||||
|
||||
Basics:
|
||||
>>> _privatize_line("MsAlice is a test person.", {'MsAlice': 'A.'})
|
||||
'<<privateperson "A.">> is a test person.'
|
||||
|
||||
>>> _privatize_line("This woman, known as MsAlice, is a test person.", \
|
||||
{'MsAlice': 'A.'})
|
||||
'This woman, known as <<privateperson "A.">>, is a test person.'
|
||||
|
||||
>>> _privatize_line("[[MsAlice]] is a test person.", {'MsAlice': 'A.'})
|
||||
'[[A.|PrivatePerson]] is a test person.'
|
||||
|
||||
>>> _privatize_line("When we talk about [[MsAlice]] in the middle of a " \
|
||||
"sentence, that's fine too.", {'MsAlice': 'A.'})
|
||||
"When we talk about [[A.|PrivatePerson]] in the middle of a sentence, that's fine too."
|
||||
|
||||
Links with different text and target:
|
||||
>>> _privatize_line("We can talk about [[Alice|MsAlice]] " \
|
||||
"with different text.", {'MsAlice': 'A.'})
|
||||
'We can talk about [[Alice|PrivatePerson]] with different text.'
|
||||
|
||||
Multiple replacements with different people:
|
||||
>>> _privatize_line("We can have [[MsAlice]] and MrBob talk to each other " \
|
||||
"in the same line.", {'MsAlice': 'A.', 'MrBob': 'B.'})
|
||||
'We can have [[A.|PrivatePerson]] and <<privateperson "B.">> talk to each other in the same line.'
|
||||
|
||||
Multiple replacements with the same person:
|
||||
>>> _privatize_line("We can have MsAlice talk to herself (MsAlice) " \
|
||||
"in the same line.", {'MsAlice': 'A.'})
|
||||
'We can have <<privateperson "A.">> talk to herself (<<privateperson "A.">>) in the same line.'
|
||||
|
||||
>>> _privatize_line("Likewise [[MsAlice]] can do it with brackets " \
|
||||
"([[MsAlice]]).", {'MsAlice': 'A.'})
|
||||
'Likewise [[A.|PrivatePerson]] can do it with brackets ([[A.|PrivatePerson]]).'
|
||||
|
||||
>>> _privatize_line('We can talk about [[Alice|MsAlice]] lots of ways, ' \
|
||||
'like MsAlice and [[MsAlice]].', {'MsAlice': 'A.'})
|
||||
'We can talk about [[Alice|PrivatePerson]] lots of ways, like <<privateperson "A.">> and [[A.|PrivatePerson]].'
|
||||
|
||||
Replacements with alternate link text:
|
||||
>>> _privatize_line('We can talk about [[Alice|MsAlice]] and [[Bob|MrBob]] as well', \
|
||||
{'MsAlice': 'A.', 'MrBob': 'B.'}, replace_link_text=True)
|
||||
'We can talk about [[A.|PrivatePerson]] and [[B.|PrivatePerson]] as well'
|
||||
|
||||
|
||||
We don't want to replace places where a CamelCase match is a substring of another
|
||||
word. This is expected to yield no output because there's nothing to replace:
|
||||
>>> _privatize_line("But an EmbeddedCamelWithMsAliceInIt isn't her.", \
|
||||
{'MsAlice': 'A.'})
|
||||
"""
|
||||
def iteroccurrences(needle: str) -> Generator[int, int, None]:
|
||||
"""
|
||||
Iterate over the start indices of occurrences of substring
|
||||
``needle`` in the line /line/ in outer scope.
|
||||
|
||||
(We have to use outer scope because it can be changed while we're iterating
|
||||
and the generator is only bound to arguments once.)
|
||||
"""
|
||||
idx = -1
|
||||
while True:
|
||||
idx = line.find(needle, idx + 1)
|
||||
if idx == -1:
|
||||
return
|
||||
else:
|
||||
additional_increments = yield idx
|
||||
if additional_increments is not None:
|
||||
idx += additional_increments
|
||||
|
||||
def anchored_at_one_end(start_index: int, end_index: int) -> bool:
|
||||
return start_index == 0 or end_index == len(line)
|
||||
|
||||
def is_camelcase_link(start_index: int, end_index: int) -> bool:
|
||||
return (anchored_at_one_end(start_index, end_index)
|
||||
or (line[start_index-1] != '[' and line[end_index] != ']'))
|
||||
|
||||
def is_bare_bracketed_link(start_index: int, end_index: int) -> bool:
|
||||
return (not anchored_at_one_end(start_index, end_index)
|
||||
and line[start_index-2:start_index] == '[['
|
||||
and line[end_index:end_index+2] == ']]')
|
||||
|
||||
def is_textual_bracketed_link(start_index: int, end_index: int) -> bool:
|
||||
return (not anchored_at_one_end(start_index, end_index)
|
||||
and line[start_index-1] == '|'
|
||||
and line[end_index:end_index+2] == ']]')
|
||||
|
||||
dirty = False
|
||||
increment_iterator_by = 0
|
||||
for replace_person, replace_initials in replacement_table.items():
|
||||
iterator = iteroccurrences(replace_person)
|
||||
try:
|
||||
while True:
|
||||
# NOTE: the "end" index is one after the last index in the string,
|
||||
# as is needed for slice notation.
|
||||
if increment_iterator_by:
|
||||
start_idx = iterator.send(increment_iterator_by)
|
||||
increment_iterator_by = 0
|
||||
else:
|
||||
start_idx = next(iterator)
|
||||
end_idx = start_idx + len(replace_person)
|
||||
new_line = None
|
||||
|
||||
if is_camelcase_link(start_idx, end_idx):
|
||||
# camel-case link or unlinked reference in text
|
||||
def is_spurious_substring():
|
||||
# If there's not a non-alphanumeric character on both sides of
|
||||
# the "link", we may be making a clbuttic replacement.
|
||||
# <https://en.wikipedia.org/wiki/Scunthorpe_problem>
|
||||
start_ok = start_idx == 0 or not line[start_idx-1].isalnum()
|
||||
end_ok = end_idx == len(line) or not line[end_idx].isalnum()
|
||||
return not (start_ok and end_ok)
|
||||
|
||||
if not is_spurious_substring():
|
||||
new_line = (line[0:start_idx]
|
||||
+ f'<<privateperson "{replace_initials}">>'
|
||||
+ line[end_idx:])
|
||||
elif is_bare_bracketed_link(start_idx, end_idx):
|
||||
# link with the person as the target and text
|
||||
replacement = replace_initials + '|PrivatePerson'
|
||||
new_line = line[0:start_idx] + replacement + line[end_idx:]
|
||||
elif is_textual_bracketed_link(start_idx, end_idx):
|
||||
# link with the person as the target only;
|
||||
# beware that you might have put something private in the text
|
||||
if replace_link_text:
|
||||
start_of_link = line[0:start_idx].rfind('[[', 0, start_idx) + 2
|
||||
new_line = line[0:start_of_link] + f"{replace_initials}|PrivatePerson" + line[end_idx:]
|
||||
else:
|
||||
new_line = line[0:start_idx] + 'PrivatePerson' + line[end_idx:]
|
||||
else:
|
||||
link = line[start_idx:end_idx]
|
||||
raise ValueError("Unknown type of link '{link}'.")
|
||||
|
||||
if new_line:
|
||||
line = new_line
|
||||
dirty = True
|
||||
# If we changed the length of the string by modifying it,
|
||||
# we need to update our stored position within the string.
|
||||
increment_iterator_by = len(new_line) - len(line)
|
||||
except StopIteration:
|
||||
pass
|
||||
|
||||
if dirty:
|
||||
return line
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
@tzk_builder
|
||||
def replace_private_people(initialer: Callable[[str], str] = None) -> None:
|
||||
def replace_private_people(initialer: Callable[[str], str] = None, replace_link_text: bool = False) -> None:
|
||||
"""
|
||||
Replace the names of people who are not marked Public with their initials.
|
||||
|
||||
@ -384,11 +535,24 @@ def replace_private_people(initialer: Callable[[str], str] = None) -> None:
|
||||
that takes one string argument
|
||||
(a tiddler filename without the full path, e.g., ``MsJaneDoe.tid``)
|
||||
and returns a string to be considered the "initials" of that person.
|
||||
|
||||
:param replace_link_text: If you have links in the form
|
||||
``So then [[John said|MrJohnDoe]] something about this``,
|
||||
then enabling this option ensures that the link is fully
|
||||
replaced with
|
||||
``So then [[J.D.|PrivatePerson]] something about this``.
|
||||
This means that when using this feature, having the
|
||||
link text also be meaningful after redaction is important.
|
||||
|
||||
.. warning ::
|
||||
Using this link replacement feature does not redact everything, just the link
|
||||
(and the link text with `replace_link_text` enabled). So *do not* rely on it
|
||||
for redacting everything. Making a tiddler public still needs consideration and
|
||||
tooling is there to help, not to replace your own judgment.
|
||||
"""
|
||||
assert 'public_wiki_folder' in build_state
|
||||
|
||||
replacement_table = _private_people_replacement_table(initialer)
|
||||
from pprint import pprint; pprint(replacement_table)
|
||||
tid_files = (Path(build_state['public_wiki_folder']) / "tiddlers").glob("**/*.tid")
|
||||
|
||||
for tiddler in tid_files:
|
||||
@ -396,25 +560,9 @@ def replace_private_people(initialer: Callable[[str], str] = None) -> None:
|
||||
with tiddler.open() as f:
|
||||
lines = f.readlines()
|
||||
for i in range(len(lines)):
|
||||
for replace_person, replace_initials in replacement_table.items():
|
||||
if replace_person in lines[i]:
|
||||
if '|' + replace_person + ']]' in lines[i]:
|
||||
# link with the person as the target only;
|
||||
# beware that you might have put something private in the text
|
||||
lines[i] = lines[i].replace(replace_person, 'PrivatePerson')
|
||||
elif '[[' + replace_person + ']]' in lines[i]:
|
||||
# link with the person as the target and text
|
||||
lines[i] = lines[i].replace(
|
||||
replace_person,
|
||||
replace_initials + '|PrivatePerson')
|
||||
else:
|
||||
# camel-case link or unlinked reference in text;
|
||||
# or spurious substring, so rule that out with the '\b' search
|
||||
lines[i] = re.sub(
|
||||
r"\b" + re.escape(replace_person) + r"\b",
|
||||
f'<<privateperson "{replace_initials}">>',
|
||||
lines[i]
|
||||
)
|
||||
private_line = _privatize_line(lines[i], replacement_table, replace_link_text)
|
||||
if private_line is not None:
|
||||
lines[i] = private_line
|
||||
dirty = True
|
||||
if dirty:
|
||||
with tiddler.open("w") as f:
|
||||
|
@ -35,6 +35,10 @@ commit_remote = ""
|
||||
# http://localhost:8080 in your browser.
|
||||
listen_port = 8080
|
||||
|
||||
# Host to listen on. If you specify "0.0.0.0" it will listen to all network interfaces.
|
||||
# This is useful for allowing the wiki to be exposed to the network through a container.
|
||||
listen_host = "127.0.0.1"
|
||||
|
||||
# Uncomment if you want to require HTTP basic authentication when serving your wiki.
|
||||
# **WARNING**: this is NOT secure for use over the open Internet or all but the
|
||||
# simplest local networks, as the password is sent in the clear. For good
|
||||
|
@ -8,7 +8,7 @@ type: text/vnd.tiddlywiki
|
||||
|
||||
<$set name="tr-rendering" value="yes">
|
||||
|
||||
<span id="tr-version">1.3.2</span>
|
||||
<span id="tr-version">1.3.3</span>
|
||||
|
||||
{{||$:/plugins/sobjornstad/TiddlyRemember/templates/AnkiDecks}}
|
||||
{{||$:/plugins/sobjornstad/TiddlyRemember/templates/AnkiTags}}
|
||||
|
@ -2,7 +2,7 @@
|
||||
"title": "$:/plugins/sobjornstad/TiddlyRemember",
|
||||
"description": "TiddlyRemember: Embed Anki notes in your TiddlyWiki",
|
||||
"author": "Soren Bjornstad",
|
||||
"version": "1.3.2",
|
||||
"version": "1.3.3",
|
||||
"core-version": ">=5.1.21",
|
||||
"source": "https://github.com/sobjornstad/TiddlyRemember",
|
||||
"list": "readme license",
|
||||
|
@ -1,5 +1,7 @@
|
||||
created: 20200516190911842
|
||||
modified: 20211113234932630
|
||||
creator: soren
|
||||
modified: 20220302210205566
|
||||
modifier: soren
|
||||
tags:
|
||||
title: $:/config/TiddlyRemember/TagMapping
|
||||
type: text/vnd.tiddlywiki
|
||||
|
@ -1,6 +1,6 @@
|
||||
created: 20200118003731285
|
||||
creator: soren
|
||||
modified: 20200118003737882
|
||||
modified: 20220215235820508
|
||||
modifier: soren
|
||||
title: $:/config/Toolbar/ButtonClass
|
||||
type: text/vnd.tiddlywiki
|
||||
|
@ -2,7 +2,7 @@ caption: Spoiler banner
|
||||
created: 20210622003118415
|
||||
creator: soren
|
||||
description: Display a warning banner on fiction tiddlers (any tiddler with a non-empty `universe` field) noting that we don't try to hide spoilers.
|
||||
modified: 20211107181812051
|
||||
modified: 20220201042246695
|
||||
modifier: soren
|
||||
private: no
|
||||
public: no
|
||||
|
@ -3,7 +3,7 @@ created: 20200419143537510
|
||||
creator: soren
|
||||
description: Copy the name of this tiddler to the clipboard
|
||||
list-after: $:/core/ui/Buttons/info
|
||||
modified: 20210922125723154
|
||||
modified: 20220215235957457
|
||||
modifier: soren
|
||||
tags: $:/tags/ViewToolbar
|
||||
title: $:/sib/Buttons/CopyTitleReference
|
||||
@ -12,7 +12,7 @@ type: text/vnd.tiddlywiki
|
||||
\whitespace trim
|
||||
<$button message="tm-copy-to-clipboard" param={{!!title}} tooltip={{$:/sib/Buttons/CopyTitleReference!!caption}} class=<<tv-config-toolbar-class>>>
|
||||
<$list filter="[<tv-config-toolbar-icons>match[yes]]">
|
||||
<i class="far fa-copy" style="font-size:160%; position:relative; bottom:-4px; left:-1px;"/>
|
||||
<i class="far fa-copy" style="font-size: 160%; position:relative; bottom:-4px; left:-1px;"/>
|
||||
</$list>
|
||||
<$list filter="[<tv-config-toolbar-text>match[yes]]">
|
||||
<span class="tc-btn-text">
|
||||
|
@ -1,7 +1,7 @@
|
||||
created: 20211120164840100
|
||||
creator: soren
|
||||
description: Navigate to the parent or a sibling of the current subtiddler. Subtiddler names are separated from that of their supertiddlers by a / (and are not system tiddlers).
|
||||
modified: 20211120170254112
|
||||
modified: 20220202173655863
|
||||
modifier: soren
|
||||
tags: $:/tags/ViewTemplate
|
||||
title: $:/sib/Templates/Automatic/Subtiddler
|
||||
@ -10,6 +10,7 @@ type: text/vnd.tiddlywiki
|
||||
|
||||
\define expand-siblings() <$action-setfield $tiddler="$:/temp/ShowSiblings" $index=<<currentTiddler>> $value="yes" />
|
||||
\define contract-siblings() <$action-setfield $tiddler="$:/temp/ShowSiblings" $index=<<currentTiddler>> $value="no" />
|
||||
\define siblings-filter() [prefix<parentTiddlerPlusSlash>!match<currentTiddler>]
|
||||
|
||||
<$list filter="[all[current]!is[system]regexp:title[/]]" variable=_>
|
||||
<$set name="parentTiddler" value={{{ [all[current]split[/]butlast[]join[/]] }}}>
|
||||
@ -18,11 +19,13 @@ type: text/vnd.tiddlywiki
|
||||
|
||||
This is a subtiddler of <$link to=<<parentTiddler>>/>.<br>
|
||||
<$reveal stateTitle="$:/temp/ShowSiblings" stateIndex=<<currentTiddler>> type="nomatch" text="yes">
|
||||
<$button class="tc-btn-invisible tc-tiddlylink" actions=<<expand-siblings>>>{{$:/core/images/right-arrow}} Siblings of this subtiddler</$button>
|
||||
<$list filter="[prefix<parentTiddlerPlusSlash>!match<currentTiddler>first[]]" variable=_ emptyMessage="This is the only subtiddler.">
|
||||
<$button class="tc-btn-invisible tc-tiddlylink" actions=<<expand-siblings>>>{{$:/core/images/right-arrow}} Siblings of this subtiddler</$button> (<$count filter=<<siblings-filter>>/>)
|
||||
</$list>
|
||||
</$reveal>
|
||||
<$reveal stateTitle="$:/temp/ShowSiblings" stateIndex=<<currentTiddler>> type="match" text="yes">
|
||||
<$button class="tc-btn-invisible tc-tiddlylink" actions=<<contract-siblings>>>{{$:/core/images/down-arrow}} Siblings of this subtiddler:</$button><br>
|
||||
<$list filter="[prefix<parentTiddlerPlusSlash>]">
|
||||
<$list filter=<<siblings-filter>>>
|
||||
<$link to=<<currentTiddler>>><$text text={{{ [<currentTiddler>removeprefix<parentTiddlerPlusSlash>] }}}/></$link><br>
|
||||
</$list>
|
||||
</$reveal>
|
||||
|
@ -10,7 +10,7 @@ import sys
|
||||
from typing import Any, Callable, Dict, NoReturn
|
||||
|
||||
|
||||
TZK_VERSION = "0.1.4"
|
||||
TZK_VERSION = "0.2.0"
|
||||
|
||||
|
||||
class BuildError(Exception):
|
||||
|
Loading…
Reference in New Issue
Block a user