diff --git a/CHANGELOG.md b/CHANGELOG.md index 58d94f7..6ce0f75 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,10 +2,13 @@ ## 0.3.0 -- Add auth loaders i.e. `sqlalchemy.require_auth_base()`, `flask_sqlalchemy` +- Add SQLAlchemy auth loaders i.e. `sqlalchemy.require_auth_base()`, `flask_sqlalchemy`. + What auth loaders do is loading user token and signature into app +- Implement `UserSigner()` - Improve JSON handling in `flask_restx` -- Add base2048 (i.e. BIP-39) codec -- Add `split_bits()` and `join_bits()` +- Add base2048 (i.e. [BIP-39](https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki)) codec +- Add `split_bits()`, `join_bits()`, `ltuple()`, `rtuple()` +- Add `markdown` extensions ## 0.2.3 diff --git a/pyproject.toml b/pyproject.toml index d452ebd..36540a6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,7 +44,9 @@ flask_sqlalchemy = [ peewee = [ "peewee>=3.0.0, <4.0" ] - +markdown = [ + "markdown>=3.0.0" +] [tool.setuptools.dynamic] version = { attr = "suou.__version__" } diff --git a/src/suou/__init__.py b/src/suou/__init__.py index 6328936..8b87268 100644 --- a/src/suou/__init__.py +++ b/src/suou/__init__.py @@ -25,7 +25,7 @@ from .classtools import Wanted, Incomplete from .itertools import makelist, kwargs_prefix, ltuple, rtuple from .i18n import I18n, JsonI18n, TomlI18n -__version__ = "0.3.0-dev21" +__version__ = "0.3.0-dev22" __all__ = ( 'Siq', 'SiqCache', 'SiqType', 'SiqGen', 'StringCase', diff --git a/src/suou/bits.py b/src/suou/bits.py index 8ab2053..0288c0f 100644 --- a/src/suou/bits.py +++ b/src/suou/bits.py @@ -50,8 +50,6 @@ def count_ones(n: int) -> int: def split_bits(buf: bytes, nbits: int) -> list[int]: ''' Split a bytestring into chunks of equal size, and interpret each chunk as an unsigned integer. - - XXX DOES NOT WORK DO NOT USE!!!!!!!! ''' mem = memoryview(buf) chunk_size = nbits // math.gcd(nbits, 8) diff --git a/src/suou/markdown.py b/src/suou/markdown.py new file mode 100644 index 0000000..58ce15d --- /dev/null +++ b/src/suou/markdown.py @@ -0,0 +1,89 @@ +""" +Plugins for markdown. + +--- + +Copyright (c) 2025 Sakuragasaki46. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +See LICENSE for the specific language governing permissions and +limitations under the License. + +This software is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +""" + +import re +import markdown +from markdown.inlinepatterns import InlineProcessor, SimpleTagInlineProcessor +import xml.etree.ElementTree as etree + +class StrikethroughExtension(markdown.extensions.Extension): + """ + Turn ~~crossed out~~ (with double tilde) text into HTML strikethrough + + well, markdown-strikethrough is a trivial dependency lol + """ + def extendMarkdown(self, md: markdown.Markdown, md_globals=None): + postprocessor = StrikethroughPostprocessor(md) + md.postprocessors.register(postprocessor, 'strikethrough', 0) + +class StrikethroughPostprocessor(markdown.postprocessors.Postprocessor): + PATTERN = re.compile(r"~~(((?!~~).)+)~~", re.DOTALL) + + def run(self, html): + return re.sub(self.PATTERN, self.convert, html) + + def convert(self, match: re.Match): + return '' + match.group(1) + '' + + +class SpoilerExtension(markdown.extensions.Extension): + """ + Add spoiler tags to text, using >!Reddit syntax!<. + + XXX remember to call SpoilerExtension.patch_blockquote_processor() + to clear conflicts with the blockquote processor and allow + spoiler tags to start at beginning of line. + """ + def extendMarkdown(self, md: markdown.Markdown, md_globals=None): + md.inlinePatterns.register(SimpleTagInlineProcessor(r'()>!(.*?)!<', 'span class="spoiler"'), 'spoiler', 14) + + @classmethod + def patch_blockquote_processor(cls): + """Patch BlockquoteProcessor to make Spoiler prevail over blockquotes.""" + from markdown.blockprocessors import BlockQuoteProcessor + BlockQuoteProcessor.RE = re.compile(r'(^|\n)[ ]{0,3}>(?!!)[ ]?(.*)') + + +class MentionPattern(InlineProcessor): + def __init__(self, regex, url_prefix: str): + super().__init__(regex) + self.url_prefix = url_prefix + def handleMatch(self, m, data): + el = etree.Element('a') + el.attrib['href'] = self.url_prefix + m.group(1) + el.text = m.group(0) + return el, m.start(0), m.end(0) + +class PingExtension(markdown.extensions.Extension): + """ + Convert @mentions into profile links. + + Customizable by passing a dict as mappings= argument, where + the key is the first character, and the value is the URL prefix. + """ + mappings: dict[str, str] + DEFAULT_MAPPINGS = {'@': '/@'} + CHARACTERS = r'[a-zA-Z0-9_-]{2,32}' + + def __init__(self, /, mappings: dict | None = None, **kwargs): + super().__init__(**kwargs) + self.mappings = mappings or self.DEFAULT_MAPPINGS.copy() + def extendMarkdown(self, md: markdown.Markdown, md_globals=None): + for at, url_prefix in self.mappings.items(): + md.inlinePatterns.register(MentionPattern(re.escape(at) + r'(' + self.CHARACTERS + ')', url_prefix), 'ping_mention', 14) + + +__all__ = ('PingExtension', 'SpoilerExtension', 'StrikethroughExtension')