diff --git a/conftest.py b/conftest.py new file mode 100644 index 0000000..e69de29 diff --git a/guild/migrations/0015_alter_character_name_alter_npc_name.py b/guild/migrations/0015_alter_character_name_alter_npc_name.py new file mode 100644 index 0000000..7a1bf57 --- /dev/null +++ b/guild/migrations/0015_alter_character_name_alter_npc_name.py @@ -0,0 +1,22 @@ +# Generated by Django 4.2.1 on 2023-08-25 20:42 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("guild", "0014_alter_playsession_npcs"), + ] + + operations = [ + migrations.AlterField( + model_name="character", + name="name", + field=models.CharField(max_length=255, unique=True), + ), + migrations.AlterField( + model_name="npc", + name="name", + field=models.CharField(max_length=255, unique=True), + ), + ] diff --git a/guild/models.py b/guild/models.py index 10fc08a..18aefb9 100644 --- a/guild/models.py +++ b/guild/models.py @@ -77,7 +77,7 @@ class Character(models.Model): RETIRED = "RETIRED", _("Retired") UNKNOWN = "UNKNOWN", _("Unknown") - name = models.CharField(max_length=255) + name = models.CharField(max_length=255, unique=True) description = models.TextField() status = models.CharField( max_length=16, choices=Status.choices, default=Status.ALIVE @@ -106,6 +106,10 @@ class Character(models.Model): def __str__(self): return self.name + @classmethod + def get_by_name(cls, name): + return cls.objects.filter(name=name).get() + def get_absolute_url(self): return reverse("guild:character_detail", kwargs={"pk": self.pk}) @@ -203,7 +207,7 @@ class Reward(models.Model): class NPC(models.Model): - name = models.CharField(max_length=255) + name = models.CharField(max_length=255, unique=True) description = models.TextField() picture = models.ImageField( @@ -222,3 +226,7 @@ class NPC(models.Model): def get_absolute_url(self): return reverse("guild:npc_detail", kwargs={"pk": self.pk}) + + @classmethod + def get_by_name(cls, name): + return cls.objects.filter(name=name).get() diff --git a/guild/templatetags/guild_extras.py b/guild/templatetags/guild_extras.py index 19d926a..bf2bb75 100644 --- a/guild/templatetags/guild_extras.py +++ b/guild/templatetags/guild_extras.py @@ -1,6 +1,7 @@ import markdown from django import template from django.template.defaultfilters import stringfilter +from guild_md.entity_links import GuildLinkExtension register = template.Library() @@ -8,4 +9,4 @@ register = template.Library() @register.filter(name="md") @stringfilter def md(value): - return markdown.markdown(value) + return markdown.markdown(value, extensions=[GuildLinkExtension()]) diff --git a/guild_md/__init__.py b/guild_md/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/guild_md/entity_links.py b/guild_md/entity_links.py new file mode 100644 index 0000000..6617f2a --- /dev/null +++ b/guild_md/entity_links.py @@ -0,0 +1,68 @@ +""" +Guild Journal Extension for Python-Markdown +====================================== + +Converts [[NPC/Frankie]] to links. + +Original code Copyright [Waylan Limberg](http://achinghead.com/). + +All changes Copyright The Python Markdown Project + +License: [BSD](https://opensource.org/licenses/bsd-license.php) + +""" + +from markdown.extensions import Extension +from markdown.inlinepatterns import InlineProcessor +from guild.models import NPC, Character +import xml.etree.ElementTree as etree + + +def build_url(category: str, name): + """Build a URL from the label, a base, and an end.""" + if category.lower() == "char": + try: + return Character.get_by_name(name).get_absolute_url() + except Character.DoesNotExist: + return "" + elif category.lower() == "npc": + try: + return NPC.get_by_name(name).get_absolute_url() + except NPC.DoesNotExist: + return "" + return "" + + +class GuildLinkExtension(Extension): + def extendMarkdown(self, md): + self.md = md + + # append to end of inline patterns + GUILD_RE = r"\[\[([\w0-9_ -]+)\/([\w0-9_ -]+)\]\]" + wikilinkPattern = GuildLinksInlineProcessor(GUILD_RE, self.getConfigs()) + wikilinkPattern.md = md + md.inlinePatterns.register(wikilinkPattern, "guildlinks", 75) + + +class GuildLinksInlineProcessor(InlineProcessor): + def __init__(self, pattern, config): + super().__init__(pattern) + self.config = config + + def handleMatch(self, m, data): + if m.group(1).strip() and m.group(2).strip(): + category = m.group(1).strip() + name = m.group(2).strip() + url = build_url(category, name) + a = etree.Element("a") + a.text = name + a.set("href", url) + if url == "": + a.set("class", "invalid-url") + else: + a = "" + return a, m.start(0), m.end(0) + + +def makeExtension(**kwargs): # pragma: no cover + return GuildLinkExtension(**kwargs) diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..15e297f --- /dev/null +++ b/pytest.ini @@ -0,0 +1,4 @@ + +[pytest] +DJANGO_SETTINGS_MODULE = guild_journal.settings +python_files = tests.py test_*.py *_tests.py diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_markdown_extension.py b/tests/test_markdown_extension.py new file mode 100644 index 0000000..ac9ffed --- /dev/null +++ b/tests/test_markdown_extension.py @@ -0,0 +1,48 @@ +import pytest +import markdown +from guild_md.entity_links import GuildLinkExtension +from guild.models import NPC, Character + + +@pytest.mark.parametrize( + ["objs", "md", "html"], + [ + [ + [], + "", + "", + ], + [ + [Character(name="Blimm")], + "[[Char/Blimm]]", + '
', + ], + [ + [Character(name="Blimm")], + "[[Char/Lun]]", + '', + ], + [ + [Character(name="Blimm"), Character(name="Lun")], + "[[Char/Lun]] and [[Char/Blimm]]", + '', + ], + [ + [NPC(name="Frank")], + "[[NPC/Frank]]", + '', + ], + [ + [NPC(name="Frank"), Character(name="Blimm")], + "[[Char/Blimm]] and [[NPC/Frank]]", + '', + ], + ], +) +@pytest.mark.django_db +def test_link_conversion(objs, md, html): + for obj in objs: + obj.save() + + output = markdown.markdown(md, extensions=[GuildLinkExtension()]) + assert output == html