Compare commits

..

5 Commits

Author SHA1 Message Date
5897a90a99 Merge branch 'privatization-fixes'
Signed-off-by: Jacob Kiers <code@kiers.eu>
2022-03-16 00:06:21 +01:00
77e649f747 Add feature to also redact custom link text
It is possible that a private person is linked to in a tiddler with the
syntax `[[Jane|MsJaneDoe]]`.

Up to now, the text of the link was not redacted, which could lead to
unintentional privacy leaks.

Therefore, a new parameter `replace_text` is introduced for the
`replace_private_people` builder.

When that is set to True, then the link text is also replaced with the
initials.

Signed-off-by: Jacob Kiers <code@kiers.eu>
2022-03-15 23:59:28 +01:00
77886cb1c6 Merge branch 'allow-specifying-listening-host' 2022-03-12 02:32:57 +01:00
57394668be Merge branch 'privatization-fixes' 2022-03-12 02:32:46 +01:00
4075c4fb18 Add feature to also redact custom link text
It is possible that a private person is linked to in a tiddler with the
syntax `[[Jane|MsJaneDoe]]`.

Up to now, the text of the link was not redacted, which could lead to
unintentional privacy leaks.

Therefore, a new parameter `replace_text` is introduced for the
`replace_private_people` builder.

When that is set to True, then the link text is also replaced with the
initials.

Signed-off-by: Jacob Kiers <code@kiers.eu>
2022-03-12 01:50:05 +01:00
16 changed files with 56 additions and 199 deletions

View File

@ -1,4 +1,4 @@
Copyright © 2021-2022 Soren Bjornstad and the tzk community.
Copyright © 2021 Soren Bjornstad.
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:

View File

@ -18,9 +18,9 @@ copyright = '2021 Soren Bjornstad'
author = 'Soren Bjornstad'
# The short X.Y version
version = "0.2.0"
version = "0.1.4"
# The full version, including alpha/beta/rc tags
release = "0.2.0"
release = "0.1.4"
# -- General configuration ---------------------------------------------------

File diff suppressed because one or more lines are too long

View File

@ -4,7 +4,6 @@
# 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

View File

@ -9,7 +9,7 @@ with open("README.md", "r") as fh:
setuptools.setup(
name="tzk",
version="0.2.0",
version="0.1.4",
author="Soren I. Bjornstad",
author_email="zettelkasten@sorenbjornstad.com",
description="Build tool for TiddlyWiki Zettelkasten",

View File

@ -13,8 +13,6 @@ 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):
"""
@ -189,7 +187,7 @@ class VersionCommand(CliCommand):
pass
def execute(self, args: argparse.Namespace) -> None:
print(VERSION_INFO)
print(f"tzk version {TZK_VERSION}")
class BuildCommand(CliCommand):
@ -394,7 +392,6 @@ 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'])
@ -411,17 +408,11 @@ def launch():
parser = argparse.ArgumentParser(
description=f"TiddlyZettelKasten {TZK_VERSION} CLI\n"
f"Copyright (c) 2021-2022 Soren Bjornstad and the tzk community.\n"
f"Copyright (c) 2021 Soren Bjornstad.\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__):

View File

@ -19,7 +19,7 @@ import re
import shutil
import subprocess
import tempfile
from typing import Callable, Dict, Generator, List, Optional, Set, Sequence, Tuple
from typing import Callable, Dict, List, Optional, Set, Sequence, Tuple
from tzk import git
from tzk import tw
@ -359,159 +359,8 @@ 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, replace_link_text: bool = False) -> None:
def replace_private_people(initialer: Callable[[str], str] = None, replace_link_text = False) -> None:
"""
Replace the names of people who are not marked Public with their initials.
@ -553,6 +402,7 @@ def replace_private_people(initialer: Callable[[str], str] = None, replace_link_
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:
@ -560,9 +410,35 @@ def replace_private_people(initialer: Callable[[str], str] = None, replace_link_
with tiddler.open() as f:
lines = f.readlines()
for i in range(len(lines)):
private_line = _privatize_line(lines[i], replacement_table, replace_link_text)
if private_line is not None:
lines[i] = private_line
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
if replace_link_text:
# with this option, the initials are also
# put in the text, solving the warning before
end = lines[i].find('|' + replace_person + ']]')
start = lines[i].rfind('[[', 0, end) + 2
search = f"[[{lines[i][start:end]}|{replace_person}]]"
replace = f"[[{replace_initials}|PrivatePerson]]"
lines[i] = lines[i].replace(search, replace)
else:
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]
)
dirty = True
if dirty:
with tiddler.open("w") as f:

View File

@ -35,10 +35,6 @@ 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

View File

@ -8,7 +8,7 @@ type: text/vnd.tiddlywiki
<$set name="tr-rendering" value="yes">
<span id="tr-version">1.3.3</span>
<span id="tr-version">1.3.2</span>
{{||$:/plugins/sobjornstad/TiddlyRemember/templates/AnkiDecks}}
{{||$:/plugins/sobjornstad/TiddlyRemember/templates/AnkiTags}}

View File

@ -2,7 +2,7 @@
"title": "$:/plugins/sobjornstad/TiddlyRemember",
"description": "TiddlyRemember: Embed Anki notes in your TiddlyWiki",
"author": "Soren Bjornstad",
"version": "1.3.3",
"version": "1.3.2",
"core-version": ">=5.1.21",
"source": "https://github.com/sobjornstad/TiddlyRemember",
"list": "readme license",

View File

@ -1,7 +1,5 @@
created: 20200516190911842
creator: soren
modified: 20220302210205566
modifier: soren
modified: 20211113234932630
tags:
title: $:/config/TiddlyRemember/TagMapping
type: text/vnd.tiddlywiki

View File

@ -1,6 +1,6 @@
created: 20200118003731285
creator: soren
modified: 20220215235820508
modified: 20200118003737882
modifier: soren
title: $:/config/Toolbar/ButtonClass
type: text/vnd.tiddlywiki

View File

@ -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: 20220201042246695
modified: 20211107181812051
modifier: soren
private: no
public: no

View File

@ -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: 20220215235957457
modified: 20210922125723154
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">

View File

@ -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: 20220202173655863
modified: 20211120170254112
modifier: soren
tags: $:/tags/ViewTemplate
title: $:/sib/Templates/Automatic/Subtiddler
@ -10,7 +10,6 @@ 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[/]] }}}>
@ -19,13 +18,11 @@ type: text/vnd.tiddlywiki
This is a subtiddler of <$link to=<<parentTiddler>>/>.<br>
<$reveal stateTitle="$:/temp/ShowSiblings" stateIndex=<<currentTiddler>> type="nomatch" text="yes">
<$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>
<$button class="tc-btn-invisible tc-tiddlylink" actions=<<expand-siblings>>>{{$:/core/images/right-arrow}} Siblings of this subtiddler</$button>
</$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=<<siblings-filter>>>
<$list filter="[prefix<parentTiddlerPlusSlash>]">
<$link to=<<currentTiddler>>><$text text={{{ [<currentTiddler>removeprefix<parentTiddlerPlusSlash>] }}}/></$link><br>
</$list>
</$reveal>

View File

@ -10,7 +10,7 @@ import sys
from typing import Any, Callable, Dict, NoReturn
TZK_VERSION = "0.2.0"
TZK_VERSION = "0.1.4"
class BuildError(Exception):