Add userlink priorities and clean up

Also added Twitter as a supported website
This commit is contained in:
Bad Manners 2023-06-30 16:46:16 -03:00
parent 8311391ba2
commit 0ae3e97186
4 changed files with 202 additions and 112 deletions

View file

@ -1,3 +1,67 @@
# upload-generator
Script to help with generating gallery uploads.
Script to generate multi-gallery upload-ready files.
## Requirements
- A Python environment to install dependencies (`pip install -r requirements.txt`); if unsure, create a fresh one with `virtualenv venv`.
- LibreOffice 6.0+, making sure that `libreoffice` is in your PATH.
## Usage
Run with `python main.py -h` for options. Generated files are output to `./out/`.
### Story files
When generating an .RTF file from the source text, the script expects that LibreOffice's style has "Preformatted Text" for plaintext, and "Normal" as the intended style to replace it with. Unless you've tinkered with LibreOffice's default formatting, this won't be an issue.
### Description files
In order to parse descriptions, you need a configuration file (default path is `./config.json`) with the websites you wish to upload to and your username there. For example:
```json
{
"aryion": "MyUsername",
"furaffinity": "My_Username",
"inkbunny": "MyUsername",
"sofurry": "My Username",
"twitter": "MyUsername",
"weasyl": "MyUsername"
}
```
Uppercase letters are optional. Only include your username for websites that you wish to generate descriptions for.
Input descriptions should be formatted as BBCode. The following tags are accepted:
```bbcode
[b]Bold text[/b]
[i]Italic text[/i]
[url=https://github.com]URL link[/url]
```
There are also special tags to link to yourself or other users automatically:
```bbcode
[self][/self]
[eka]EkasPortalUser[/eka]
[fa]FurAffinityUser[/fa]
[weasyl]WeasylUser[/weasyl]
[ib]InkbunnyUser[/ib]
[sf]SoFurryUser[/sf]
[twitter]TwitterUser[/twitter]
```
`[self]` tags must always be empty. The other tags are nestable and flexible, allowing attributes to display information differently on each supported website. Some examples:
```bbcode
[eka=Lorem][/eka] is equivalent to [eka]Lorem[/eka].
[fa=Ipsum]Dolor[/fa] shows Ipsum's username on FurAffinity, and Dolor everywhere else as a link to Ipsum's FA userpage.
[weasyl=Sit][ib=Amet][/ib][/weasyl] will show the two user links on Weasyl and Inkbunny as expected. For other websites, the innermost tag is prioritized - Inkbunny, in this case.
[ib=Amet][weasyl=Sit][/weasyl][/ib] is the same as above, but the Weasyl is prioritized instead.
[ib=Amet][weasyl=Sit]Consectetur[/weasyl][/ib] is the same as above, but Consectetur is displayed as the username for websites other than Inkbunny and Weasyl. The Weasyl gallery is linked to in those websites.
```

8
example_config.json Normal file
View file

@ -0,0 +1,8 @@
{
"aryion": "MyUsername",
"furaffinity": "My_Username",
"inkbunny": "MyUsername",
"sofurry": "My Username",
"twitter": "MyUsername",
"weasyl": "MyUsername"
}

10
main.py
View file

@ -21,7 +21,7 @@ def get_rtf_styles(rtf_source: str):
rtf_styles[style_name] = rtf_style
return rtf_styles
def main(story_path=None, description_path=None, keep_out_dir=False, ignore_empty_files=False):
def main(story_path=None, description_path=None, config_path='./config.json', keep_out_dir=False, ignore_empty_files=False):
remove_out_dir = not keep_out_dir and os.path.isdir(OUT_DIR)
with tempfile.TemporaryDirectory() as tdir:
# Clear OUT_DIR if it exists and shouldn't be kept
@ -88,7 +88,7 @@ def main(story_path=None, description_path=None, keep_out_dir=False, ignore_empt
print(f'Ignoring error ({error})')
else:
raise RuntimeError(error)
parse_description(desc, OUT_DIR)
parse_description(desc, config_path, OUT_DIR)
except subprocess.CalledProcessError as e:
if remove_out_dir:
@ -110,7 +110,9 @@ if __name__ == '__main__':
parser.add_argument('-s', '--story', dest='story_path',
help='path of LibreOffice-readable story file')
parser.add_argument('-d', '--description', dest='description_path',
help='path of LibreOffice-readable description file')
help='path of BBCode-formatted description file')
parser.add_argument('-c', '--config', dest='config_path', default='./config.json',
help='path of JSON configuration file')
parser.add_argument('-k', '--keep-out-dir', dest='keep_out_dir', action='store_true',
help='whether output directory contents should be kept')
parser.add_argument('-i', '--ignore-empty-files', dest='ignore_empty_files', action='store_true',
@ -123,5 +125,7 @@ if __name__ == '__main__':
parser.error('--story must be a valid file')
if args.description_path and not os.path.isfile(args.description_path):
parser.error('--description must be a valid file')
if args.config_path and not os.path.isfile(args.config_path):
parser.error('--config must be a valid file')
main(**vars(args))

230
parse.py
View file

@ -1,6 +1,10 @@
from collections import OrderedDict
import json
import lark
import os
import typing
SUPPORTED_USER_TAGS = ('eka', 'fa', 'weasyl', 'ib', 'sf', 'twitter')
DESCRIPTION_GRAMMAR = r"""
?start: document_list
@ -17,25 +21,16 @@ DESCRIPTION_GRAMMAR = r"""
b_tag: "[b]" [document_list] "[/b]"
i_tag: "[i]" [document_list] "[/i]"
url_tag: "[url" ["=" [URL]] "]" [document_list] "[/url]"
self_tag: "[self]" [WS] "[/self]"
user_tag_root: user_tag
user_tag: eka_tag
| fa_tag
| weasyl_tag
| ib_tag
| sf_tag
user_tag: """
eka_tag: "[eka" ["=" USERNAME] "]" USERNAME "[/eka]"
| "[eka" "=" USERNAME "]" [user_tag] "[/eka]"
fa_tag: "[fa" ["=" USERNAME] "]" USERNAME "[/fa]"
| "[fa" "=" USERNAME "]" [user_tag] "[/fa]"
weasyl_tag: "[weasyl" ["=" USERNAME] "]" USERNAME "[/weasyl]"
| "[weasyl" "=" USERNAME "]" [user_tag] "[/weasyl]"
ib_tag: "[ib" ["=" USERNAME] "]" USERNAME "[/ib]"
| "[ib" "=" USERNAME "]" [user_tag] "[/ib]"
sf_tag: "[sf" ["=" USERNAME] "]" USERNAME "[/sf]"
| "[sf" "=" USERNAME "]" [user_tag] "[/sf]"
DESCRIPTION_GRAMMAR += ' | '.join(f'{tag}_tag' for tag in SUPPORTED_USER_TAGS)
DESCRIPTION_GRAMMAR += ''.join(f'\n {tag}_tag: "[{tag}" ["=" USERNAME] "]" USERNAME "[/{tag}]" | "[{tag}" "=" USERNAME "]" [user_tag] "[/{tag}]"' for tag in SUPPORTED_USER_TAGS)
DESCRIPTION_GRAMMAR += r"""
USERNAME: /[a-zA-Z0-9][a-zA-Z0-9 _-]*/
URL: /(https?:\/\/)?[^\]]+/
@ -48,15 +43,49 @@ DESCRIPTION_PARSER = lark.Lark(DESCRIPTION_GRAMMAR, parser='lalr')
class UserTag:
def __init__(self, default=None, eka=None, fa=None, weasyl=None, ib=None, sf=None):
self.default = default
self.eka = eka
self.fa = fa
self.weasyl = weasyl
self.ib = ib
self.sf = sf
def __init__(self, default=None, **kwargs):
self.default: typing.Optional[str] = default
self._sites: typing.OrderedDict[str, typing.Optional[str]] = OrderedDict()
for (k, v) in kwargs.items():
if k in SUPPORTED_USER_TAGS:
self.__setitem__(k, v)
def __setitem__(self, name: str, value: typing.Optional[str]) -> None:
if name in self._sites:
if value is None:
self._sites.pop(name)
else:
self._sites[name] = value
elif value is not None:
self._sites[name] = value
def __getitem__(self, name: str) -> typing.Optional[str]:
return self._sites.get(name)
@property
def sites(self):
yield from self._sites
class UploadTransformer(lark.Transformer):
def __init__(self, *args, **kwargs):
super(UploadTransformer, self).__init__(*args, **kwargs)
def _user_tag_factory(tag):
def user_tag(data):
attribute, inner = data[0], data[1]
if attribute and attribute.strip():
if isinstance(inner, UserTag):
inner[tag] = attribute.strip()
return inner
user = UserTag(default=inner and inner.strip())
user[tag] = attribute.strip()
return user
user = UserTag()
user[tag] = inner.strip()
return user
return user_tag
for tag in SUPPORTED_USER_TAGS:
setattr(self, f'{tag}_tag', _user_tag_factory(tag))
def document_list(self, data):
return ''.join(data)
@ -77,66 +106,24 @@ class UploadTransformer(lark.Transformer):
def user_tag_root(self, data):
user_data: UserTag = data[0]
if user_data.fa:
return self.url_tag((f'https://furaffinity.net/user/{user_data.fa.replace("_", "")}', user_data.default or user_data.fa))
if user_data.eka:
return self.url_tag((f'https://aryion.com/g4/user/{user_data.eka}', user_data.default or user_data.eka))
if user_data.ib:
return self.url_tag((f'https://inkbunny.net/{user_data.ib}', user_data.default or user_data.ib))
if user_data.sf:
return self.url_tag((f'https://{user_data.sf.replace(" ", "-").lower()}.sofurry.com', user_data.default or user_data.sf))
if user_data.weasyl:
self.url_tag((f'https://www.weasyl.com/~{user_data.weasyl}', user_data.default or user_data.weasyl))
for site in user_data.sites:
if site == 'eka':
return self.url_tag((f'https://aryion.com/g4/user/{user_data["eka"]}', user_data.default or user_data["eka"]))
if site == 'fa':
return self.url_tag((f'https://furaffinity.net/user/{user_data["fa"].replace("_", "")}', user_data.default or user_data['fa']))
if site == 'ib':
return self.url_tag((f'https://inkbunny.net/{user_data["ib"]}', user_data.default or user_data['ib']))
if site == 'sf':
return self.url_tag((f'https://{user_data["sf"].replace(" ", "-").lower()}.sofurry.com', user_data.default or user_data['sf']))
if site == 'twitter':
return self.url_tag((f'https://twitter.com/{user_data["twitter"]}', user_data.default or user_data['twitter']))
if site == 'weasyl':
self.url_tag((f'https://www.weasyl.com/~{user_data["weasyl"].replace(" ", "").lower()}', user_data.default or user_data['weasyl']))
raise TypeError('Invalid UserTag data')
def user_tag(self, data):
return data[0]
def eka_tag(self, data):
attribute, inner = data[0], data[1]
if attribute and attribute.strip():
if isinstance(inner, UserTag):
inner.eka = attribute.strip()
return inner
return UserTag(eka=attribute.strip(), default=inner and inner.strip())
return UserTag(eka=inner.strip())
def fa_tag(self, data):
attribute, inner = data[0], data[1]
if attribute and attribute.strip():
if isinstance(inner, UserTag):
inner.fa = attribute.strip()
return inner
return UserTag(fa=attribute.strip(), default=inner and inner.strip())
return UserTag(fa=inner.strip())
def weasyl_tag(self, data):
attribute, inner = data[0], data[1]
if attribute and attribute.strip():
if isinstance(inner, UserTag):
inner.weasyl = attribute.strip()
return inner
return UserTag(weasyl=attribute.strip(), default=inner and inner.strip())
return UserTag(weasyl=inner.strip())
def ib_tag(self, data):
attribute, inner = data[0], data[1]
if attribute and attribute.strip():
if isinstance(inner, UserTag):
inner.ib = attribute.strip()
return inner
return UserTag(ib=attribute.strip(), default=inner and inner.strip())
return UserTag(ib=inner.strip())
def sf_tag(self, data):
attribute, inner = data[0], data[1]
if attribute and attribute.strip():
if isinstance(inner, UserTag):
inner.sf = attribute.strip()
return inner
return UserTag(sf=attribute.strip(), default=inner and inner.strip())
return UserTag(sf=inner.strip())
class BbcodeTransformer(UploadTransformer):
def b_tag(self, data):
if data[0] is None or not data[0].strip():
@ -165,6 +152,18 @@ class MarkdownTransformer(UploadTransformer):
def url_tag(self, data):
return f'[{data[1] or ""}]({data[0] or ""})'
class PlaintextTransformer(UploadTransformer):
def b_tag(self, data):
return f'{data[0] or ""}'
def i_tag(self, data):
return f'{data[0] or ""}'
def url_tag(self, data):
if data[1] is None or not data[1].strip():
return f'{data[0] or ""}'
return f'{data[1].strip()}: {data[0] or ""}'
class AryionTransformer(BbcodeTransformer):
def __init__(self, this_user, *args, **kwargs):
super(AryionTransformer, self).__init__(*args, **kwargs)
@ -172,8 +171,8 @@ class AryionTransformer(BbcodeTransformer):
def user_tag_root(self, data):
user_data = data[0]
if user_data.eka:
return f':icon{user_data.eka}:'
if user_data['eka']:
return f':icon{user_data["eka"]}:'
return super(AryionTransformer, self).user_tag_root(data)
class FuraffinityTransformer(BbcodeTransformer):
@ -183,8 +182,8 @@ class FuraffinityTransformer(BbcodeTransformer):
def user_tag_root(self, data):
user_data = data[0]
if user_data.fa:
return f':icon{user_data.fa}:'
if user_data['fa']:
return f':icon{user_data["fa"]}:'
return super(FuraffinityTransformer, self).user_tag_root(data)
class WeasylTransformer(MarkdownTransformer):
@ -194,15 +193,16 @@ class WeasylTransformer(MarkdownTransformer):
def user_tag_root(self, data):
user_data = data[0]
if user_data.weasyl:
return f'<!~{user_data.weasyl}>'
if user_data['weasyl']:
return f'<!~{user_data["weasyl"].replace(" ", "")}>'
if user_data.default is None:
if user_data.fa:
return f'<fa:{user_data.fa}>'
if user_data.ib:
return f'<ib:{user_data.ib}>'
if user_data.sf:
return f'<sf:{user_data.sf}>'
for site in user_data.sites:
if site == 'fa':
return f'<fa:{user_data["fa"]}>'
if site == 'ib':
return f'<ib:{user_data["ib"]}>'
if site == 'sf':
return f'<sf:{user_data["sf"]}>'
return super(WeasylTransformer, self).user_tag_root(data)
class InkbunnyTransformer(BbcodeTransformer):
@ -212,15 +212,16 @@ class InkbunnyTransformer(BbcodeTransformer):
def user_tag_root(self, data):
user_data = data[0]
if user_data.ib:
return f'[iconname]{user_data.ib}[/iconname]'
if user_data['ib']:
return f'[iconname]{user_data["ib"]}[/iconname]'
if user_data.default is None:
if user_data.fa:
return f'[fa]{user_data.fa}[/fa]'
if user_data.sf:
return f'[sf]{user_data.sf}[/sf]'
if user_data.weasyl:
return f'[weasyl]{user_data.weasyl}[/weasyl]'
for site in user_data.sites:
if site == 'fa':
return f'[fa]{user_data["fa"]}[/fa]'
if site == 'sf':
return f'[sf]{user_data["sf"]}[/sf]'
if site == 'weasyl':
return f'[weasyl]{user_data["weasyl"].replace(" ", "").lower()}[/weasyl]'
return super(InkbunnyTransformer, self).user_tag_root(data)
class SoFurryTransformer(BbcodeTransformer):
@ -230,27 +231,40 @@ class SoFurryTransformer(BbcodeTransformer):
def user_tag_root(self, data):
user_data = data[0]
if user_data.sf:
return f':icon{user_data.sf}:'
if user_data['sf']:
return f':icon{user_data["sf"]}:'
if user_data.default is None:
if user_data.fa:
return f'fa!{user_data.fa}'
if user_data.ib:
return f'ib!{user_data.ib}'
for site in user_data.sites:
if site == 'fa':
return f'fa!{user_data["fa"]}'
if site == 'ib':
return f'ib!{user_data["ib"]}'
return super(SoFurryTransformer, self).user_tag_root(data)
class TwitterTransformer(PlaintextTransformer):
def __init__(self, this_user, *args, **kwargs):
super(TwitterTransformer, self).__init__(*args, **kwargs)
self.self_tag = lambda _: self.user_tag_root((UserTag(twitter=this_user),))
def user_tag_root(self, data):
user_data = data[0]
if user_data['twitter']:
return f'@{user_data["twitter"]}'
return super(TwitterTransformer, self).user_tag_root(data)
TRANSFORMATIONS = {
'furaffinity': ('desc_furaffinity.txt', FuraffinityTransformer),
'aryion': ('desc_aryion.txt', AryionTransformer),
'weasyl': ('desc_weasyl.md', WeasylTransformer),
'furaffinity': ('desc_furaffinity.txt', FuraffinityTransformer),
'inkbunny': ('desc_inkbunny.txt', InkbunnyTransformer),
'sofurry': ('desc_sofurry.txt', SoFurryTransformer),
'twitter': ('desc_twitter.txt', TwitterTransformer),
'weasyl': ('desc_weasyl.md', WeasylTransformer),
}
def parse_description(description, out_dir):
def parse_description(description, config_path, out_dir):
parsed_description = DESCRIPTION_PARSER.parse(description)
with open('config.json', 'r') as f:
with open(config_path, 'r') as f:
config = json.load(f)
# Validate JSON
errors = []