diff --git a/.gitignore b/.gitignore index 86b5b6fb..aeb51408 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ __pycache__ build/ dist/ +venv/ *.egg-info/ *.pypirc *.pyc diff --git a/Dockerfile b/Dockerfile index 99cf333e..da755ef4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,20 +2,27 @@ FROM alpine:3.16.0 WORKDIR /app -RUN set -xe; +# Install required packages including tini +RUN set -xe && \ + apk add --no-cache python3 py3-pip tini && \ + pip install --upgrade pip setuptools-scm +# Copy project files COPY . . -RUN apk add --no-cache python3 py3-pip tini; \ - pip install --upgrade pip setuptools-scm; \ - python3 setup.py install; \ - python3 martor_demo/manage.py makemigrations; \ - python3 martor_demo/manage.py migrate; \ - addgroup -g 1000 appuser; \ - adduser -u 1000 -G appuser -D -h /app appuser; \ +# Install Python dependencies and setup Django app +RUN python3 setup.py install && \ + python3 martor_demo/manage.py makemigrations && \ + python3 martor_demo/manage.py migrate + +# Create user and set permissions +RUN addgroup -g 1000 appuser && \ + adduser -u 1000 -G appuser -D -h /app appuser && \ chown -R appuser:appuser /app USER appuser EXPOSE 8000/tcp -ENTRYPOINT [ "tini", "--" ] -CMD [ "python3", "/app/martor_demo/manage.py", "runserver", "0.0.0.0:8000" ] + +# Use full path for tini +ENTRYPOINT ["/sbin/tini", "--"] +CMD ["python3", "/app/martor_demo/manage.py", "runserver", "0.0.0.0:8000"] diff --git a/README.md b/README.md index ccf00210..d39b2b43 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ * Supports Django Admin * Toolbar Buttons * Highlight `pre` +* Custom ID Attributes (Add custom IDs to any text element using `{#custom-id}` syntax, e.g., `# Heading1 {#my-h1-id}`, for easy linking and navigation. ### Preview @@ -140,6 +141,7 @@ MARTOR_MARKDOWN_EXTENSIONS = [ 'martor.extensions.emoji', # to parse markdown emoji 'martor.extensions.mdx_video', # to parse embed/iframe video 'martor.extensions.escape_html', # to handle the XSS vulnerabilities + "martor.extensions.mdx_add_id", # to parse id like {#this_is_id} ] # Markdown Extensions Configs diff --git a/martor/extensions/mdx_add_id.py b/martor/extensions/mdx_add_id.py new file mode 100644 index 00000000..5be25ad2 --- /dev/null +++ b/martor/extensions/mdx_add_id.py @@ -0,0 +1,37 @@ +import markdown +from xml.etree import ElementTree + +# Regex pattern to detect `{#id_name}` at the end of the line +ADD_ID_RE = r"(.+?)\s\{#([a-zA-Z0-9_-]+)\}$" + + +class AddIDPattern(markdown.inlinepatterns.Pattern): + """Pattern to match Markdown text ending with `{#id}` and set it as an ID.""" + + def handleMatch(self, m): + text_content = m.group(2).strip() # Actual text content + id_value = m.group(3) # The ID inside `{#id}` + + # Create a element to hold the text and ID + el = ElementTree.Element("span") + el.text = markdown.util.AtomicString(text_content) + el.set("id", id_value) + return el + + +class AddIDExtension(markdown.Extension): + """Add ID Extension for Python-Markdown.""" + + def extendMarkdown(self, md: markdown.core.Markdown, *args): + """Register AddIDPattern with the Markdown parser.""" + md.inlinePatterns.register(AddIDPattern(ADD_ID_RE, md), "add_id", 9) + + +def makeExtension(*args, **kwargs): + return AddIDExtension(*args, **kwargs) + + +if __name__ == "__main__": + import doctest + + doctest.testmod() diff --git a/martor/settings.py b/martor/settings.py index 4bbb9a35..c3bbf56b 100644 --- a/martor/settings.py +++ b/martor/settings.py @@ -77,6 +77,7 @@ "martor.extensions.emoji", # to parse markdown emoji "martor.extensions.mdx_video", # to parse embed/iframe video "martor.extensions.escape_html", # to handle the XSS vulnerabilities + "martor.extensions.mdx_add_id", # to parse id like {#this_is_id} ], ) diff --git a/martor/tests/tests.py b/martor/tests/tests.py index aa5cfac0..ac317c9a 100644 --- a/martor/tests/tests.py +++ b/martor/tests/tests.py @@ -110,6 +110,19 @@ def test_markdownify(self): # f'

{self.user.username}

', # ) + # Id + response = self.client.post( + "/martor/markdownify/", + { + "content": "__Advertisement :)__ {#ad-section}\n###### h6 Heading {#h6-heading}" + }, + ) + self.assertEqual(response.status_code, 200) + self.assertEqual( + response.content.decode("utf-8"), + '

Advertisement :)

\n
h6 Heading
', + ) # noqa: E501 + def test_markdownify_xss_handled(self): xss_payload_1 = "[aaaa](javascript:alert(1))" response_1 = markdownify(xss_payload_1)