From 0ae3e97186df9aea49a07ad7df5b5623499473b5 Mon Sep 17 00:00:00 2001 From: Bad Manners Date: Fri, 30 Jun 2023 16:46:16 -0300 Subject: [PATCH] Add userlink priorities and clean up Also added Twitter as a supported website --- README.md | 66 ++++++++++++- example_config.json | 8 ++ main.py | 10 +- parse.py | 230 +++++++++++++++++++++++--------------------- 4 files changed, 202 insertions(+), 112 deletions(-) create mode 100644 example_config.json diff --git a/README.md b/README.md index 7bef701..2c425e5 100644 --- a/README.md +++ b/README.md @@ -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. +``` diff --git a/example_config.json b/example_config.json new file mode 100644 index 0000000..bfc26d6 --- /dev/null +++ b/example_config.json @@ -0,0 +1,8 @@ +{ + "aryion": "MyUsername", + "furaffinity": "My_Username", + "inkbunny": "MyUsername", + "sofurry": "My Username", + "twitter": "MyUsername", + "weasyl": "MyUsername" +} \ No newline at end of file diff --git a/main.py b/main.py index 3242a62..e639bf8 100644 --- a/main.py +++ b/main.py @@ -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)) diff --git a/parse.py b/parse.py index 52343e2..e822218 100644 --- a/parse.py +++ b/parse.py @@ -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'' + if user_data['weasyl']: + return f'' if user_data.default is None: - if user_data.fa: - return f'' - if user_data.ib: - return f'' - if user_data.sf: - return f'' + for site in user_data.sites: + if site == 'fa': + return f'' + if site == 'ib': + return f'' + if site == 'sf': + return f'' 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 = []